Commit af32d275 by Enkelmann Committed by Enkelmann

Adapt the analysis modules to use the internal IR instead of the BAP IR.

parent d701731b
......@@ -5,11 +5,13 @@ all:
cp target/release/libcwe_checker_rs.so src/dllcwe_checker_rs.so
dune build
dune install
cd plugins/cwe_checker; make all; cd ../..
cd plugins/cwe_checker_emulation; make all; cd ../..
cd plugins/cwe_checker_type_inference; make all; cd ../..
cd plugins/cwe_checker_type_inference_print; make all; cd ../..
cd plugins/cwe_checker_pointer_inference_debug; make all; cd ../..
cd plugins/cwe_checker && make all
cd plugins/cwe_checker_emulation && make all
cd plugins/cwe_checker_type_inference && make all
cd plugins/cwe_checker_type_inference_print && make all
cd plugins/cwe_checker_pointer_inference_debug && make all
mkdir ${HOME}/.config/cwe_checker
cp src/utils/registers.json ${HOME}/.config/cwe_checker/registers.json
test:
cargo test
......@@ -43,6 +45,7 @@ uninstall:
cd plugins/cwe_checker_type_inference; make uninstall; cd ../..
cd plugins/cwe_checker_type_inference_print; make uninstall; cd ../..
cd plugins/cwe_checker_pointer_inference_debug; make uninstall; cd ../..
rm -f -r ${HOME}/.config/cwe_checker
documentation:
dune build @doc
......
......@@ -15,6 +15,7 @@ fnv = "1.0" # a faster hash function for small keys like integers
anyhow = "1.0" # for easy error types
crossbeam-channel = "0.4"
derive_more = "0.99"
directories = "3.0"
[lib]
name = "cwe_checker_rs"
......
use crate::bil::variable::*;
use crate::intermediate_representation::*;
use crate::prelude::*;
use derive_more::Deref;
use std::sync::Arc;
......@@ -57,7 +57,7 @@ impl std::fmt::Display for AbstractIdentifier {
/// It is also impossible to accidently describe circular references.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
pub enum AbstractLocation {
Register(String, BitSize),
Register(String, ByteSize),
Pointer(String, AbstractMemoryLocation),
}
......@@ -81,7 +81,7 @@ impl AbstractLocation {
}
Ok(AbstractLocation::Register(
variable.name.clone(),
variable.bitsize()?,
variable.size,
))
}
}
......
//! This module defines traits describing general properties of abstract domains
//! as well as several abstract domain types implementing these traits.
use crate::bil::*;
use crate::intermediate_representation::*;
mod bitvector;
pub use bitvector::*;
......@@ -29,14 +29,14 @@ pub trait AbstractDomain: Sized + Eq + Clone {
fn is_top(&self) -> bool;
}
/// A trait for types representing values with a fixed size (in bits).
/// A trait for types representing values with a fixed size (in bytes).
///
/// For abstract domains, the bitsize is a parameter of the domain itself,
/// i.e. you cannot merge values of different bitsizes,
/// since they lie in different posets (one for each bitsize).
pub trait HasBitSize {
/// Return the size of the represented value in bits.
fn bitsize(&self) -> BitSize;
/// For abstract domains, the bytesize is a parameter of the domain itself,
/// i.e. you cannot merge values of different bytesizes,
/// since they lie in different posets (one for each bytesize).
pub trait HasByteSize {
/// Return the size of the represented value in bytes.
fn bytesize(&self) -> ByteSize;
}
/// An abstract domain implementing this trait has a global maximum, i.e. a *Top* element.
......@@ -52,11 +52,11 @@ pub trait HasTop {
/// A trait for abstract domains that can represent values loaded into CPU register.
///
/// The domain implements all general operations used to manipulate register values.
/// The domain is parametrized by its bitsize (which represents the size of the register).
/// It has a *Top* element, which is only characterized by its bitsize.
pub trait RegisterDomain: AbstractDomain + HasBitSize + HasTop {
/// Return a new top element with the given bitsize
fn new_top(bitsize: BitSize) -> Self;
/// The domain is parametrized by its bytesize (which represents the size of the register).
/// It has a *Top* element, which is only characterized by its bytesize.
pub trait RegisterDomain: AbstractDomain + HasByteSize + HasTop {
/// Return a new top element with the given bytesize
fn new_top(bytesize: ByteSize) -> Self;
/// Compute the (abstract) result of a binary operation
fn bin_op(&self, op: BinOpType, rhs: &Self) -> Self;
......@@ -64,12 +64,9 @@ pub trait RegisterDomain: AbstractDomain + HasBitSize + HasTop {
/// Compute the (abstract) result of a unary operation
fn un_op(&self, op: UnOpType) -> Self;
/// extract a sub-bitvector
fn extract(&self, low_bit: BitSize, high_bit: BitSize) -> Self;
/// Extract a sub-bitvector
fn subpiece(&self, low_byte: ByteSize, size: ByteSize) -> Self;
/// Extend a bitvector using the given cast type
fn cast(&self, kind: CastType, width: BitSize) -> Self;
/// Concatenate two bitvectors
fn concat(&self, other: &Self) -> Self;
/// Perform a typecast to extend a bitvector or to cast between integer and floating point types.
fn cast(&self, kind: CastOpType, width: ByteSize) -> Self;
}
use super::{AbstractDomain, AbstractIdentifier, HasBitSize, RegisterDomain};
use crate::bil::BinOpType;
use super::{AbstractDomain, AbstractIdentifier, HasByteSize, RegisterDomain};
use crate::intermediate_representation::{BinOpType, ByteSize};
use crate::prelude::*;
use std::collections::BTreeMap;
use std::fmt::Display;
......@@ -38,15 +38,15 @@ impl<T: RegisterDomain> AbstractDomain for PointerDomain<T> {
}
}
impl<T: RegisterDomain> HasBitSize for PointerDomain<T> {
impl<T: RegisterDomain> HasByteSize for PointerDomain<T> {
/// Return the bitsize of the pointer.
/// Should always equal the pointer size of the CPU architecture.
fn bitsize(&self) -> BitSize {
fn bytesize(&self) -> ByteSize {
self.0
.values()
.next()
.expect("Pointer without targets encountered")
.bitsize()
.bytesize()
}
}
......@@ -84,7 +84,7 @@ impl<T: RegisterDomain> PointerDomain<T> {
offset_adjustment: &T,
) {
if let Some(old_offset) = self.0.get(&old_id) {
let new_offset = old_offset.bin_op(BinOpType::PLUS, offset_adjustment);
let new_offset = old_offset.bin_op(BinOpType::IntAdd, offset_adjustment);
self.0.remove(old_id);
self.0.insert(new_id.clone(), new_offset);
}
......@@ -94,7 +94,7 @@ impl<T: RegisterDomain> PointerDomain<T> {
pub fn add_to_offset(&self, value: &T) -> PointerDomain<T> {
let mut result = self.clone();
for offset in result.0.values_mut() {
*offset = offset.bin_op(BinOpType::PLUS, value);
*offset = offset.bin_op(BinOpType::IntAdd, value);
}
result
}
......@@ -103,7 +103,7 @@ impl<T: RegisterDomain> PointerDomain<T> {
pub fn sub_from_offset(&self, value: &T) -> PointerDomain<T> {
let mut result = self.clone();
for offset in result.0.values_mut() {
*offset = offset.bin_op(BinOpType::MINUS, value);
*offset = offset.bin_op(BinOpType::IntSub, value);
}
result
}
......@@ -149,7 +149,7 @@ mod tests {
fn new_id(name: &str) -> AbstractIdentifier {
AbstractIdentifier::new(
Tid::new("time0"),
AbstractLocation::Register(name.into(), 64),
AbstractLocation::Register(name.into(), ByteSize::new(8)),
)
}
......
......@@ -39,8 +39,8 @@
//! The artificial *CallReturn* nodes enable enriching the information flowing through a return edge
//! with information recovered from the corresponding callsite during a fixpoint computation.
use crate::intermediate_representation::*;
use crate::prelude::*;
use crate::term::*;
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::{HashMap, HashSet};
......@@ -166,19 +166,22 @@ impl<'a> GraphBuilder<'a> {
jump: &'a Term<Jmp>,
untaken_conditional: Option<&'a Term<Jmp>>,
) {
match &jump.term.kind {
JmpKind::Goto(Label::Direct(tid)) => {
match &jump.term {
Jmp::Branch(tid)
| Jmp::CBranch {
target: tid,
condition: _,
} => {
self.graph.add_edge(
source,
self.jump_targets[&tid].0,
Edge::Jump(jump, untaken_conditional),
);
}
JmpKind::Goto(Label::Indirect(_)) => (), // TODO: add handling of indirect edges!
JmpKind::Call(ref call) => {
if let Label::Direct(ref target_tid) = call.target {
if self.extern_subs.contains(target_tid) {
if let Some(Label::Direct(ref return_tid)) = call.return_ {
Jmp::BranchInd(_) => (), // TODO: add handling of indirect edges!
Jmp::Call { target, return_ } => {
if self.extern_subs.contains(target) {
if let Some(return_tid) = return_ {
self.graph.add_edge(
source,
self.jump_targets[&return_tid].0,
......@@ -186,27 +189,32 @@ impl<'a> GraphBuilder<'a> {
);
}
} else {
if let Some(target) = self.jump_targets.get(&target_tid) {
self.graph.add_edge(source, target.0, Edge::Call(jump));
}
if let Some(Label::Direct(ref return_tid)) = call.return_ {
if let Some(target_node) = self.jump_targets.get(&target) {
self.graph.add_edge(source, target_node.0, Edge::Call(jump));
} // TODO: Log message for the else-case?
if let Some(ref return_tid) = return_ {
let return_index = self.jump_targets[return_tid].0;
self.return_addresses
.entry(target_tid.clone())
.entry(target.clone())
.and_modify(|vec| vec.push((source, return_index)))
.or_insert_with(|| vec![(source, return_index)]);
}
// TODO: Non-returning calls and tail calls both have no return target in BAP.
// Thus we need to distinguish them somehow to correctly handle tail calls.
}
}
Jmp::CallInd {
target: _,
return_: _,
} => {
// TODO: add handling of indirect calls!
}
Jmp::CallOther {
description: _,
return_: _,
} => {
// TODO: Decide how to represent CallOther edges.
// Right now they are dead ends in the control flow graph.
}
JmpKind::Interrupt {
value: _,
return_addr: _,
} => (), // TODO: Add some handling for interrupts
JmpKind::Return(_) => {} // return edges are handled in a different function
Jmp::Return(_) => {} // return edges are handled in a different function
}
}
......@@ -244,7 +252,7 @@ impl<'a> GraphBuilder<'a> {
.term
.jmps
.iter()
.find(|jump| matches!(jump.term.kind, JmpKind::Call(_)))
.find(|jump| matches!(jump.term, Jmp::Call{..}))
.unwrap();
let cr_combine_node = self.graph.add_node(Node::CallReturn {
call: call_block,
......@@ -267,7 +275,7 @@ impl<'a> GraphBuilder<'a> {
.term
.jmps
.iter()
.any(|jmp| matches!(jmp.term.kind, JmpKind::Return(_)))
.any(|jmp| matches!(jmp.term, Jmp::Return(_)))
{
let return_from_node = self.jump_targets[&block.tid].1;
self.add_call_return_node_and_edges(sub, return_from_node);
......@@ -326,29 +334,18 @@ mod tests {
use super::*;
fn mock_program() -> Term<Program> {
use Label::*;
let call = Call {
target: Direct(Tid::new("sub2")),
return_: Some(Direct(Tid::new("sub1_blk2"))),
};
let call_term = Term {
tid: Tid::new("call".to_string()),
term: Jmp {
condition: None,
kind: JmpKind::Call(call),
term: Jmp::Call {
target: Tid::new("sub2"),
return_: Some(Tid::new("sub1_blk2")),
},
};
let return_term = Term {
tid: Tid::new("return".to_string()),
term: Jmp {
condition: None,
kind: JmpKind::Return(Direct(Tid::new("sub1_blk2"))),
},
};
let jmp = Jmp {
condition: None,
kind: JmpKind::Goto(Direct(Tid::new("sub1_blk1"))),
term: Jmp::Return(Expression::Const(Bitvector::zero(64.into()))), // The return term does not matter
};
let jmp = Jmp::Branch(Tid::new("sub1_blk1"));
let jmp_term = Term {
tid: Tid::new("jump"),
term: jmp,
......
......@@ -13,9 +13,8 @@
use super::fixpoint::Context as GeneralFPContext;
use super::graph::*;
use crate::bil::Expression;
use crate::intermediate_representation::*;
use crate::prelude::*;
use crate::term::*;
use fnv::FnvHashMap;
use petgraph::graph::{EdgeIndex, NodeIndex};
use std::marker::PhantomData;
......
use super::object::ObjectType;
use crate::abstract_domain::*;
use crate::analysis::graph::Graph;
use crate::bil::Expression;
use crate::intermediate_representation::*;
use crate::prelude::*;
use crate::term::symbol::ExternSymbol;
use crate::term::*;
use crate::utils::log::*;
use std::collections::{BTreeMap, BTreeSet, HashSet};
......@@ -84,10 +82,10 @@ impl<'a> Context<'a> {
return_term: &Term<Jmp>,
) {
let expected_stack_pointer_offset = match self.project.cpu_architecture.as_str() {
"x86" | "x86_64" => Bitvector::from_u16(self.project.get_pointer_bitsize() / 8)
.into_zero_extend(self.project.get_pointer_bitsize() as usize)
"x86" | "x86_64" => Bitvector::from_u64(u64::from(self.project.get_pointer_bytesize()))
.into_truncate(apint::BitWidth::from(self.project.get_pointer_bytesize()))
.unwrap(),
_ => Bitvector::zero((self.project.get_pointer_bitsize() as usize).into()),
_ => Bitvector::zero(apint::BitWidth::from(self.project.get_pointer_bytesize())),
};
match state_before_return.get_register(&self.project.stack_pointer_register) {
Ok(Data::Pointer(pointer)) => {
......@@ -134,21 +132,18 @@ impl<'a> Context<'a> {
call.tid.clone(),
AbstractLocation::from_var(return_register).unwrap(),
);
let address_bitsize = self.project.stack_pointer_register.bitsize().unwrap();
let address_bytesize = self.project.get_pointer_bytesize();
state.memory.add_abstract_object(
object_id.clone(),
Bitvector::zero((address_bitsize as usize).into()).into(),
Bitvector::zero(apint::BitWidth::from(address_bytesize)).into(),
super::object::ObjectType::Heap,
address_bitsize,
address_bytesize,
);
let pointer = PointerDomain::new(
object_id,
Bitvector::zero((address_bitsize as usize).into()).into(),
);
self.log_debug(
state.set_register(return_register, pointer.into()),
Some(&call.tid),
Bitvector::zero(apint::BitWidth::from(address_bytesize)).into(),
);
state.set_register(return_register, pointer.into());
Some(state)
}
Err(err) => {
......@@ -159,6 +154,27 @@ impl<'a> Context<'a> {
}
}
/// Evaluate the value of a parameter of an extern symbol for the given state.
fn eval_parameter_arg(&self, state: &State, parameter: &Arg) -> Result<Data, Error> {
match parameter {
Arg::Register(var) => state.eval(&Expression::Var(var.clone())),
Arg::Stack { offset, size } => state.load_value(
&Expression::BinOp {
op: BinOpType::IntAdd,
lhs: Box::new(Expression::Var(self.project.stack_pointer_register.clone())),
rhs: Box::new(Expression::Const(
Bitvector::from_i64(*offset)
.into_truncate(apint::BitWidth::from(
self.project.get_pointer_bytesize(),
))
.unwrap(),
)),
},
*size,
),
}
}
/// Mark the object that the parameter of a call is pointing to as freed.
/// If the object may have been already freed, generate a CWE warning.
/// This models the behaviour of `free` and similar functions.
......@@ -170,7 +186,9 @@ impl<'a> Context<'a> {
extern_symbol: &ExternSymbol,
) -> Option<State> {
match extern_symbol.get_unique_parameter() {
Ok(parameter_expression) => match state.eval(parameter_expression) {
Ok(parameter) => {
let parameter_value = self.eval_parameter_arg(state, parameter);
match parameter_value {
Ok(memory_object_pointer) => {
if let Data::Pointer(pointer) = memory_object_pointer {
if let Err(possible_double_frees) =
......@@ -206,7 +224,8 @@ impl<'a> Context<'a> {
self.log_debug(Err(err), Some(&call.tid));
Some(new_state)
}
},
}
}
Err(err) => {
// We do not know which memory object to free
self.log_debug(Err(err), Some(&call.tid));
......@@ -222,12 +241,8 @@ impl<'a> Context<'a> {
call: &Term<Jmp>,
extern_symbol: &ExternSymbol,
) {
for argument in extern_symbol
.arguments
.iter()
.filter(|arg| arg.intent.is_input())
{
match state.eval(&argument.location) {
for parameter in extern_symbol.parameters.iter() {
match self.eval_parameter_arg(state, parameter) {
Ok(value) => {
if state.memory.is_dangling_pointer(&value, true) {
let warning = CweWarning {
......@@ -247,8 +262,8 @@ impl<'a> Context<'a> {
}
Err(err) => self.log_debug(
Err(err.context(format!(
"Function argument expression {:?} could not be evaluated",
argument.location
"Function parameter {:?} could not be evaluated",
parameter
))),
Some(&call.tid),
),
......@@ -266,11 +281,11 @@ impl<'a> Context<'a> {
extern_symbol: &ExternSymbol,
) -> Option<State> {
self.log_debug(
new_state.clear_stack_parameter(extern_symbol),
new_state.clear_stack_parameter(extern_symbol, &self.project.stack_pointer_register),
Some(&call.tid),
);
let mut possible_referenced_ids = BTreeSet::new();
if extern_symbol.arguments.is_empty() {
if extern_symbol.parameters.is_empty() && extern_symbol.return_values.is_empty() {
// We assume here that we do not know the parameters and approximate them by all possible parameter registers.
// This approximation is wrong if the function is known but has neither parameters nor return values.
// We cannot distinguish these two cases yet.
......@@ -280,12 +295,8 @@ impl<'a> Context<'a> {
}
}
} else {
for parameter in extern_symbol
.arguments
.iter()
.filter(|arg| arg.intent.is_input())
{
if let Ok(data) = state.eval(&parameter.location) {
for parameter in extern_symbol.parameters.iter() {
if let Ok(data) = self.eval_parameter_arg(state, parameter) {
possible_referenced_ids.append(&mut data.referenced_ids());
}
}
......@@ -312,13 +323,13 @@ impl<'a> Context<'a> {
if *stack_id == state.stack_id {
stack_offset_domain.clone()
} else {
BitvectorDomain::new_top(stack_pointer.bitsize())
BitvectorDomain::new_top(stack_pointer.bytesize())
}
} else {
BitvectorDomain::new_top(self.project.stack_pointer_register.bitsize().unwrap())
BitvectorDomain::new_top(self.project.stack_pointer_register.size)
}
} else {
BitvectorDomain::new_top(self.project.stack_pointer_register.bitsize().unwrap())
BitvectorDomain::new_top(self.project.stack_pointer_register.size)
}
}
}
......
use super::*;
use crate::bil::variable::*;
fn bv(value: i64) -> BitvectorDomain {
BitvectorDomain::Value(Bitvector::from_i64(value))
......@@ -8,59 +7,51 @@ fn bv(value: i64) -> BitvectorDomain {
fn new_id(time: &str, reg_name: &str) -> AbstractIdentifier {
AbstractIdentifier::new(
Tid::new(time),
AbstractLocation::Register(reg_name.to_string(), 64),
AbstractLocation::Register(reg_name.to_string(), ByteSize::new(8)),
)
}
fn mock_extern_symbol(name: &str) -> ExternSymbol {
use crate::bil;
let arg = Arg {
var: register("RAX"),
location: bil::Expression::Var(register("RAX")),
intent: ArgIntent::Both,
};
let arg = Arg::Register(register("RAX"));
ExternSymbol {
tid: Tid::new("extern_".to_string() + name),
address: "somewhere".into(),
name: name.into(),
calling_convention: None,
arguments: vec![arg],
parameters: vec![arg.clone()],
return_values: vec![arg],
no_return: false,
}
}
fn register(name: &str) -> Variable {
Variable {
name: name.into(),
type_: crate::bil::variable::Type::Immediate(64),
size: ByteSize::new(8),
is_temp: false,
}
}
fn reg_add_term(name: &str, value: i64, tid_name: &str) -> Term<Def> {
let add_expr = Expression::BinOp {
op: crate::bil::BinOpType::PLUS,
op: BinOpType::IntAdd,
lhs: Box::new(Expression::Var(register(name))),
rhs: Box::new(Expression::Const(Bitvector::from_i64(value))),
};
Term {
tid: Tid::new(format!("{}", tid_name)),
term: Def {
lhs: register(name),
rhs: add_expr,
term: Def::Assign {
var: register(name),
value: add_expr,
},
}
}
fn call_term(target_name: &str) -> Term<Jmp> {
let call = Call {
target: Label::Direct(Tid::new(target_name)),
return_: None,
};
Term {
tid: Tid::new(format!("call_{}", target_name)),
term: Jmp {
condition: None,
kind: JmpKind::Call(call),
term: Jmp::Call {
target: Tid::new(target_name),
return_: None,
},
}
}
......@@ -68,10 +59,10 @@ fn call_term(target_name: &str) -> Term<Jmp> {
fn return_term(target_name: &str) -> Term<Jmp> {
Term {
tid: Tid::new(format!("return")),
term: Jmp {
condition: None,
kind: JmpKind::Return(Label::Direct(Tid::new(target_name))),
},
term: Jmp::Return(Expression::Unknown {
description: target_name.into(),
size: ByteSize::new(8),
}),
}
}
......@@ -102,7 +93,6 @@ fn mock_project() -> Project {
fn context_problem_implementation() {
use crate::analysis::interprocedural_fixpoint::Context as IpFpContext;
use crate::analysis::pointer_inference::Data;
use crate::bil::*;
use Expression::*;
let project = mock_project();
......@@ -113,10 +103,10 @@ fn context_problem_implementation() {
let def = Term {
tid: Tid::new("def"),
term: Def {
lhs: register("RSP"),
rhs: BinOp {
op: BinOpType::PLUS,
term: Def::Assign {
var: register("RSP"),
value: BinOp {
op: BinOpType::IntAdd,
lhs: Box::new(Var(register("RSP"))),
rhs: Box::new(Const(Bitvector::from_i64(-16))),
},
......@@ -124,15 +114,9 @@ fn context_problem_implementation() {
};
let store_term = Term {
tid: Tid::new("store"),
term: Def {
lhs: register("memory"), // technically false, but not checked at the moment
rhs: Store {
address: Box::new(Var(register("RSP"))),
endian: Endianness::LittleEndian,
memory: Box::new(Var(register("memory"))), // This is technically false, but the field is ignored at the moment
value: Box::new(Const(Bitvector::from_i64(42))),
size: 64,
},
term: Def::Store {
address: Var(register("RSP")),
value: Const(Bitvector::from_i64(42)),
},
};
......@@ -183,12 +167,8 @@ fn context_problem_implementation() {
state.get_register(&register("RSP")).unwrap()
);
state
.set_register(&register("callee_saved_reg"), Data::Value(bv(13)))
.unwrap();
state
.set_register(&register("other_reg"), Data::Value(bv(14)))
.unwrap();
state.set_register(&register("callee_saved_reg"), Data::Value(bv(13)));
state.set_register(&register("other_reg"), Data::Value(bv(14)));
let malloc = call_term("extern_malloc");
let mut state_after_malloc = context.update_call_stub(&state, &malloc).unwrap();
......@@ -205,7 +185,7 @@ fn context_problem_implementation() {
state
.get_register(&register("RSP"))
.unwrap()
.bin_op(BinOpType::PLUS, &Data::Value(bv(8)))
.bin_op(BinOpType::IntAdd, &Data::Value(bv(8)))
);
assert_eq!(
state_after_malloc
......@@ -218,15 +198,13 @@ fn context_problem_implementation() {
.unwrap()
.is_top());
state_after_malloc
.set_register(
state_after_malloc.set_register(
&register("callee_saved_reg"),
Data::Pointer(PointerDomain::new(
new_id("call_extern_malloc", "RAX"),
bv(0),
)),
)
.unwrap();
);
let free = call_term("extern_free");
let state_after_free = context
.update_call_stub(&state_after_malloc, &free)
......@@ -254,7 +232,7 @@ fn context_problem_implementation() {
state
.get_register(&register("RSP"))
.unwrap()
.bin_op(BinOpType::PLUS, &Data::Value(bv(8)))
.bin_op(BinOpType::IntAdd, &Data::Value(bv(8)))
);
assert_eq!(
state_after_other_fn
......@@ -290,7 +268,7 @@ fn update_return() {
callsite_id.clone(),
bv(0).into(),
ObjectType::Stack,
64,
ByteSize::new(8),
);
state_before_return
.caller_stack_ids
......@@ -304,7 +282,7 @@ fn update_return() {
other_callsite_id.clone(),
bv(0).into(),
ObjectType::Stack,
64,
ByteSize::new(8),
);
state_before_return
.caller_stack_ids
......@@ -312,15 +290,13 @@ fn update_return() {
state_before_return
.ids_known_to_caller
.insert(other_callsite_id.clone());
state_before_return
.set_register(
state_before_return.set_register(
&register("RAX"),
Data::Pointer(PointerDomain::new(
new_id("call_callee_other", "RSP"),
bv(-32),
)),
)
.unwrap();
);
let state_before_call = State::new(&register("RSP"), Tid::new("original_caller_id"));
let mut state_before_call = context
......@@ -334,7 +310,7 @@ fn update_return() {
caller_caller_id.clone(),
bv(0).into(),
ObjectType::Stack,
64,
ByteSize::new(8),
);
state_before_call
.caller_stack_ids
......
......@@ -14,8 +14,7 @@
use super::interprocedural_fixpoint::{Computation, NodeValue};
use crate::abstract_domain::{BitvectorDomain, DataDomain};
use crate::analysis::graph::{Graph, Node};
use crate::prelude::*;
use crate::term::*;
use crate::intermediate_representation::*;
use crate::utils::log::*;
use petgraph::graph::NodeIndex;
use petgraph::visit::IntoNodeReferences;
......
......@@ -22,8 +22,8 @@ impl DerefMut for AbstractObject {
impl AbstractObject {
/// Create a new abstract object with given object type and address bitsize.
pub fn new(type_: ObjectType, address_bitsize: BitSize) -> AbstractObject {
AbstractObject(Arc::new(AbstractObjectInfo::new(type_, address_bitsize)))
pub fn new(type_: ObjectType, address_bytesize: ByteSize) -> AbstractObject {
AbstractObject(Arc::new(AbstractObjectInfo::new(type_, address_bytesize)))
}
/// Short-circuits the `AbstractObjectInfo::merge` function if `self==other`.
......@@ -53,20 +53,19 @@ pub struct AbstractObjectInfo {
impl AbstractObjectInfo {
/// Create a new abstract object with known object type and address bitsize
pub fn new(type_: ObjectType, address_bitsize: BitSize) -> AbstractObjectInfo {
pub fn new(type_: ObjectType, address_bytesize: ByteSize) -> AbstractObjectInfo {
AbstractObjectInfo {
pointer_targets: BTreeSet::new(),
is_unique: true,
state: Some(ObjectState::Alive),
type_: Some(type_),
memory: MemRegion::new(address_bitsize),
memory: MemRegion::new(address_bytesize),
}
}
/// Read the value at the given offset of the given size (in bits, not bytes) inside the memory region.
pub fn get_value(&self, offset: Bitvector, bitsize: BitSize) -> Data {
assert_eq!(bitsize % 8, 0);
self.memory.get(offset, (bitsize / 8) as u64)
pub fn get_value(&self, offset: Bitvector, bytesize: ByteSize) -> Data {
self.memory.get(offset, bytesize)
}
/// Write a value at the given offset to the memory region.
......@@ -83,12 +82,12 @@ impl AbstractObjectInfo {
} else {
let merged_value = self
.memory
.get(concrete_offset.clone(), (value.bitsize() / 8) as u64)
.get(concrete_offset.clone(), value.bytesize())
.merge(&value);
self.memory.add(merged_value, concrete_offset.clone());
};
} else {
self.memory = MemRegion::new(self.memory.get_address_bitsize());
self.memory = MemRegion::new(self.memory.get_address_bytesize());
}
Ok(())
}
......@@ -101,11 +100,11 @@ impl AbstractObjectInfo {
if let BitvectorDomain::Value(ref concrete_offset) = offset {
let merged_value = self
.memory
.get(concrete_offset.clone(), (value.bitsize() / 8) as u64)
.get(concrete_offset.clone(), value.bytesize())
.merge(&value);
self.memory.add(merged_value, concrete_offset.clone());
} else {
self.memory = MemRegion::new(self.memory.get_address_bitsize());
self.memory = MemRegion::new(self.memory.get_address_bytesize());
}
}
......@@ -164,7 +163,7 @@ impl AbstractObjectInfo {
/// Represents the effect of unknown write instructions to the object
/// which may include writing pointers to targets from the `additional_targets` set to the object.
pub fn assume_arbitrary_writes(&mut self, additional_targets: &BTreeSet<AbstractIdentifier>) {
self.memory = MemRegion::new(self.memory.get_address_bitsize());
self.memory = MemRegion::new(self.memory.get_address_bytesize());
self.pointer_targets
.extend(additional_targets.iter().cloned());
}
......@@ -294,7 +293,7 @@ mod tests {
is_unique: true,
state: Some(ObjectState::Alive),
type_: Some(ObjectType::Heap),
memory: MemRegion::new(64),
memory: MemRegion::new(ByteSize::new(8)),
};
AbstractObject(Arc::new(obj_info))
}
......@@ -310,7 +309,7 @@ mod tests {
fn new_id(tid: &str, reg_name: &str) -> AbstractIdentifier {
AbstractIdentifier::new(
Tid::new(tid),
AbstractLocation::Register(reg_name.into(), 64),
AbstractLocation::Register(reg_name.into(), ByteSize::new(8)),
)
}
......@@ -321,19 +320,22 @@ mod tests {
let offset = bv(-15);
object.set_value(three, &offset).unwrap();
assert_eq!(
object.get_value(Bitvector::from_i64(-16), 64),
Data::Top(64)
object.get_value(Bitvector::from_i64(-16), ByteSize::new(8)),
Data::Top(ByteSize::new(8))
);
assert_eq!(
object.get_value(Bitvector::from_i64(-15), ByteSize::new(8)),
new_data(3)
);
assert_eq!(object.get_value(Bitvector::from_i64(-15), 64), new_data(3));
object.set_value(new_data(4), &bv(-12)).unwrap();
assert_eq!(
object.get_value(Bitvector::from_i64(-15), 64),
Data::Top(64)
object.get_value(Bitvector::from_i64(-15), ByteSize::new(8)),
Data::Top(ByteSize::new(8))
);
object.merge_value(new_data(5), &bv(-12));
assert_eq!(
object.get_value(Bitvector::from_i64(-12), 64),
Data::Value(BitvectorDomain::new_top(64))
object.get_value(Bitvector::from_i64(-12), ByteSize::new(8)),
Data::Value(BitvectorDomain::new_top(ByteSize::new(8)))
);
let mut other_object = new_abstract_object();
......@@ -341,11 +343,11 @@ mod tests {
other_object.set_value(new_data(0), &bv(0)).unwrap();
let merged_object = object.merge(&other_object);
assert_eq!(
merged_object.get_value(Bitvector::from_i64(-12), 64),
Data::Top(64)
merged_object.get_value(Bitvector::from_i64(-12), ByteSize::new(8)),
Data::Top(ByteSize::new(8))
);
assert_eq!(
merged_object.get_value(Bitvector::from_i64(0), 64),
merged_object.get_value(Bitvector::from_i64(0), ByteSize::new(8)),
new_data(0)
);
}
......@@ -370,7 +372,7 @@ mod tests {
target_map.remove(&new_id("time_1", "RAX"));
let modified_pointer = PointerDomain::with_targets(target_map);
assert_eq!(
object.get_value(Bitvector::from_i64(-15), 64),
object.get_value(Bitvector::from_i64(-15), ByteSize::new(8)),
modified_pointer.into()
);
......@@ -384,7 +386,7 @@ mod tests {
target_map.insert(new_id("time_234", "RBX"), bv(50));
let modified_pointer = PointerDomain::with_targets(target_map);
assert_eq!(
object.get_value(Bitvector::from_i64(-15), 64),
object.get_value(Bitvector::from_i64(-15), ByteSize::new(8)),
modified_pointer.into()
);
}
......
......@@ -22,15 +22,15 @@ impl AbstractObjectList {
/// The offset into the stack object will be set to zero.
pub fn from_stack_id(
stack_id: AbstractIdentifier,
address_bitsize: BitSize,
address_bytesize: ByteSize,
) -> AbstractObjectList {
let mut objects = BTreeMap::new();
let stack_object = AbstractObject::new(ObjectType::Stack, address_bitsize);
let stack_object = AbstractObject::new(ObjectType::Stack, address_bytesize);
objects.insert(
stack_id,
(
stack_object,
Bitvector::zero((address_bitsize as usize).into()).into(),
Bitvector::zero(apint::BitWidth::from(address_bytesize)).into(),
),
);
AbstractObjectList { objects }
......@@ -68,7 +68,7 @@ impl AbstractObjectList {
/// If the address is not unique, merge the value of all possible addresses.
///
/// Returns an error if the address is a `Data::Value`, i.e. not a pointer.
pub fn get_value(&self, address: &Data, size: BitSize) -> Result<Data, Error> {
pub fn get_value(&self, address: &Data, size: ByteSize) -> Result<Data, Error> {
match address {
Data::Value(value) => Err(anyhow!("Load from non-pointer value:\n{:?}", value)),
Data::Top(_) => Ok(Data::new_top(size)),
......@@ -158,9 +158,9 @@ impl AbstractObjectList {
object_id: AbstractIdentifier,
initial_offset: BitvectorDomain,
type_: ObjectType,
address_bitsize: BitSize,
address_bytesize: ByteSize,
) {
let new_object = AbstractObject::new(type_, address_bitsize);
let new_object = AbstractObject::new(type_, address_bytesize);
if let Some((object, offset)) = self.objects.get_mut(&object_id) {
// If the identifier already exists, we have to assume that more than one object may be referenced by this identifier.
object.is_unique = false;
......@@ -333,13 +333,14 @@ mod tests {
fn new_id(name: &str) -> AbstractIdentifier {
AbstractIdentifier::new(
Tid::new("time0"),
AbstractLocation::Register(name.into(), 64),
AbstractLocation::Register(name.into(), ByteSize::new(8)),
)
}
#[test]
fn abstract_object_list() {
let mut obj_list = AbstractObjectList::from_stack_id(new_id("RSP".into()), 64);
let mut obj_list =
AbstractObjectList::from_stack_id(new_id("RSP".into()), ByteSize::new(8));
assert_eq!(obj_list.objects.len(), 1);
assert_eq!(obj_list.objects.values().next().unwrap().1, bv(0));
......@@ -349,12 +350,13 @@ mod tests {
.unwrap();
assert_eq!(
obj_list
.get_value(&Data::Pointer(pointer.clone()), 64)
.get_value(&Data::Pointer(pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Value(bv(42))
);
let mut other_obj_list = AbstractObjectList::from_stack_id(new_id("RSP".into()), 64);
let mut other_obj_list =
AbstractObjectList::from_stack_id(new_id("RSP".into()), ByteSize::new(8));
let second_pointer = PointerDomain::new(new_id("RSP".into()), bv(-8));
other_obj_list
.set_value(pointer.clone(), Data::Value(bv(42)))
......@@ -364,12 +366,17 @@ mod tests {
.unwrap();
assert_eq!(
other_obj_list
.get_value(&Data::Pointer(second_pointer.clone()), 64)
.get_value(&Data::Pointer(second_pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Value(bv(35))
);
other_obj_list.add_abstract_object(new_id("RAX".into()), bv(0), ObjectType::Heap, 64);
other_obj_list.add_abstract_object(
new_id("RAX".into()),
bv(0),
ObjectType::Heap,
ByteSize::new(8),
);
let heap_pointer = PointerDomain::new(new_id("RAX".into()), bv(8));
other_obj_list
.set_value(heap_pointer.clone(), Data::Value(bv(3)))
......@@ -378,19 +385,19 @@ mod tests {
let mut merged = obj_list.merge(&other_obj_list);
assert_eq!(
merged
.get_value(&Data::Pointer(pointer.clone()), 64)
.get_value(&Data::Pointer(pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Value(bv(42))
);
assert_eq!(
merged
.get_value(&Data::Pointer(second_pointer.clone()), 64)
.get_value(&Data::Pointer(second_pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::new_top(64)
Data::new_top(ByteSize::new(8))
);
assert_eq!(
merged
.get_value(&Data::Pointer(heap_pointer.clone()), 64)
.get_value(&Data::Pointer(heap_pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Value(bv(3))
);
......@@ -401,13 +408,13 @@ mod tests {
.unwrap();
assert_eq!(
merged
.get_value(&Data::Pointer(pointer.clone()), 64)
.get_value(&Data::Pointer(pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Value(BitvectorDomain::new_top(64))
Data::Value(BitvectorDomain::new_top(ByteSize::new(8)))
);
assert_eq!(
merged
.get_value(&Data::Pointer(heap_pointer.clone()), 64)
.get_value(&Data::Pointer(heap_pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Value(bv(3))
);
......@@ -435,7 +442,7 @@ mod tests {
other_obj_list.replace_abstract_id(&new_id("RAX".into()), &new_id("ID2".into()), &bv(0));
assert_eq!(
other_obj_list
.get_value(&Data::Pointer(pointer.clone()), 64)
.get_value(&Data::Pointer(pointer.clone()), ByteSize::new(8))
.unwrap(),
Data::Pointer(modified_heap_pointer.clone())
);
......@@ -481,10 +488,16 @@ mod tests {
#[test]
fn append_unknown_objects_test() {
let mut obj_list = AbstractObjectList::from_stack_id(new_id("stack"), 64);
let mut other_obj_list = AbstractObjectList::from_stack_id(new_id("stack"), 64);
other_obj_list.add_abstract_object(new_id("heap_obj"), bv(0).into(), ObjectType::Heap, 64);
let mut obj_list = AbstractObjectList::from_stack_id(new_id("stack"), ByteSize::new(8));
let mut other_obj_list =
AbstractObjectList::from_stack_id(new_id("stack"), ByteSize::new(8));
other_obj_list.add_abstract_object(
new_id("heap_obj"),
bv(0).into(),
ObjectType::Heap,
ByteSize::new(8),
);
obj_list.append_unknown_objects(&other_obj_list);
assert_eq!(obj_list.objects.len(), 2);
......
use super::object_list::AbstractObjectList;
use super::Data;
use crate::abstract_domain::*;
use crate::bil::*;
use crate::intermediate_representation::*;
use crate::prelude::*;
use crate::term::symbol::ExternSymbol;
use std::collections::{BTreeMap, BTreeSet};
mod access_handling;
......@@ -50,16 +49,13 @@ impl State {
stack_register.clone(),
PointerDomain::new(
stack_id.clone(),
Bitvector::zero((stack_register.bitsize().unwrap() as usize).into()).into(),
Bitvector::zero(apint::BitWidth::from(stack_register.size)).into(),
)
.into(),
);
State {
register,
memory: AbstractObjectList::from_stack_id(
stack_id.clone(),
stack_register.bitsize().unwrap(),
),
memory: AbstractObjectList::from_stack_id(stack_id.clone(), stack_register.size),
stack_id,
caller_stack_ids: BTreeSet::new(),
ids_known_to_caller: BTreeSet::new(),
......@@ -89,18 +85,27 @@ impl State {
/// Mark those parameter values of an extern function call, that are passed on the stack,
/// as unknown data (since the function may modify them).
pub fn clear_stack_parameter(&mut self, extern_call: &ExternSymbol) -> Result<(), Error> {
pub fn clear_stack_parameter(
&mut self,
extern_call: &ExternSymbol,
stack_pointer_register: &Variable,
) -> Result<(), Error> {
let mut result_log = Ok(());
for arg in &extern_call.arguments {
match &arg.location {
Expression::Var(_) => {}
location_expression => {
let arg_size = arg
.var
.bitsize()
.expect("Encountered argument with unknown size");
let data_top = Data::new_top(arg_size);
if let Err(err) = self.write_to_address(location_expression, &data_top) {
for arg in &extern_call.parameters {
match arg {
Arg::Register(_) => (),
Arg::Stack { offset, size } => {
let data_top = Data::new_top(*size);
let location_expression = Expression::BinOp {
lhs: Box::new(Expression::Var(stack_pointer_register.clone())),
op: BinOpType::IntAdd,
rhs: Box::new(Expression::Const(
Bitvector::from_i64(*offset)
.into_truncate(apint::BitWidth::from(stack_pointer_register.size))
.unwrap(),
)),
};
if let Err(err) = self.write_to_address(&location_expression, &data_top) {
result_log = Err(err);
}
}
......
......@@ -12,7 +12,7 @@ fn run_pointer_inference(program_jsonbuilder_val: ocaml::Value) -> (Vec<CweWarni
serde_json::from_value(program_json).expect("Project deserialization failed");
project.replace_let_bindings();
crate::analysis::pointer_inference::run(&project, false)
crate::analysis::pointer_inference::run(&project.into(), false)
}
caml!(rs_run_pointer_inference(program_jsonbuilder_val) {
......@@ -31,7 +31,7 @@ fn run_pointer_inference_and_print_debug(program_jsonbuilder_val: ocaml::Value)
serde_json::from_value(program_json).expect("Project deserialization failed");
project.replace_let_bindings();
crate::analysis::pointer_inference::run(&project, true); // Note: This discard all CweWarnings and log messages.
crate::analysis::pointer_inference::run(&project.into(), true); // Note: This discard all CweWarnings and log messages.
}
caml!(rs_run_pointer_inference_and_print_debug(program_jsonbuilder_val) {
......
......@@ -52,6 +52,32 @@ pub enum Expression {
},
}
impl Expression {
/// Return the size (in bytes) of the result value of the expression.
pub fn bytesize(&self) -> ByteSize {
use BinOpType::*;
use Expression::*;
match self {
Var(var) => var.size,
Const(bitvec) => bitvec.width().into(),
BinOp { op, lhs, rhs } => match op {
Piece => lhs.bytesize() + rhs.bytesize(),
IntEqual | IntNotEqual | IntLess | IntSLess | IntLessEqual | IntSLessEqual
| IntCarry | IntSCarry | IntSBorrow | BoolXOr | BoolOr | BoolAnd | FloatEqual
| FloatNotEqual | FloatLess | FloatLessEqual => ByteSize::new(1),
IntAdd | IntSub | IntAnd | IntOr | IntXOr | IntLeft | IntRight | IntSRight
| IntMult | IntDiv | IntRem | IntSDiv | IntSRem | FloatAdd | FloatSub
| FloatMult | FloatDiv => lhs.bytesize(),
},
UnOp { op, arg } => match op {
UnOpType::FloatNaN => ByteSize::new(1),
_ => arg.bytesize(),
},
Cast { size, .. } | Unknown { size, .. } | Subpiece { size, .. } => *size,
}
}
}
/// The type/mnemonic of a binary operation
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum BinOpType {
......
......@@ -73,6 +73,12 @@ impl From<ByteSize> for apint::BitWidth {
}
}
impl From<apint::BitWidth> for ByteSize {
fn from(bitwidth: apint::BitWidth) -> ByteSize {
ByteSize::new(bitwidth.to_usize() as u64 / 8)
}
}
impl ByteSize {
pub fn new(value: u64) -> ByteSize {
ByteSize(value)
......
......@@ -181,6 +181,30 @@ pub struct ExternSymbol {
pub no_return: bool,
}
impl ExternSymbol {
/// If the extern symbol has exactly one return value that is passed in a register,
/// return the register.
pub fn get_unique_return_register(&self) -> Result<&Variable, Error> {
if self.return_values.len() == 1 {
match self.return_values[0] {
Arg::Register(ref var) => Ok(var),
Arg::Stack { .. } => Err(anyhow!("Return value is passed on the stak")),
}
} else {
Err(anyhow!("Wrong number of return values"))
}
}
/// If the extern symbol has exactly one parameter, return the parameter.
pub fn get_unique_parameter(&self) -> Result<&Arg, Error> {
if self.parameters.len() == 1 {
Ok(&self.parameters[0])
} else {
Err(anyhow!("Wrong number of parameter values"))
}
}
}
/// The `Program` structure represents a disassembled binary.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
pub struct Program {
......@@ -205,4 +229,19 @@ pub struct Project {
pub cpu_architecture: String,
/// The stack pointer register for the given CPU architecture.
pub stack_pointer_register: Variable,
/// The names of callee-saved registers for the standard calling convention
/// for the given CPU architecture.
/// Note that this field may be removed in the future.
pub callee_saved_registers: Vec<String>,
/// The names of parameter registers for the standard calling convention
/// for the given CPU architecture.
/// Note that this field may be removed in the future.
pub parameter_registers: Vec<String>,
}
impl Project {
/// Return the size (in bytes) for pointers of the given architecture.
pub fn get_pointer_bytesize(&self) -> ByteSize {
self.stack_pointer_register.size
}
}
......@@ -9,7 +9,7 @@ use crate::prelude::*;
/// Temporary variables are only valid until the end of the current assembly instruction.
/// However, one assembly instruction may span more than one basic block in the intermediate representation
/// (but never more than one function).
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct Variable {
pub name: String,
pub size: ByteSize,
......
......@@ -21,6 +21,7 @@ mod prelude {
pub use serde::{Deserialize, Serialize};
pub use crate::bil::{BitSize, Bitvector};
pub use crate::intermediate_representation::ByteSize;
pub use crate::intermediate_representation::{Term, Tid};
pub use anyhow::{anyhow, Error};
}
......@@ -375,10 +375,15 @@ impl From<Project> for IrProject {
tid: project.program.tid,
term: project.program.term.into(),
};
let (params, callee_saved) = crate::utils::get_generic_parameter_and_callee_saved_register(
&project.cpu_architecture,
);
IrProject {
program,
cpu_architecture: project.cpu_architecture,
stack_pointer_register: project.stack_pointer_register.into(),
callee_saved_registers: callee_saved,
parameter_registers: params,
}
}
}
......@@ -657,11 +662,11 @@ mod tests {
}
},
"stack_pointer_register": {
"name": "ESP",
"size": 32,
"name": "RSP",
"size": 8,
"is_virtual": false
},
"cpu_architecture": "x86_32"
"cpu_architecture": "x86_64"
}
"#,
)
......
......@@ -358,6 +358,8 @@ impl From<Project> for IrProject {
program,
cpu_architecture: project.cpu_architecture,
stack_pointer_register: project.stack_pointer_register.into(),
callee_saved_registers: project.callee_saved_registers,
parameter_registers: project.parameter_registers,
}
}
}
......
pub mod log;
/// Get the names of parameter registers and callee saved registers
/// of the standard calling convention for the given architecture.
///
/// The registers are read from a configuration file.
pub fn get_generic_parameter_and_callee_saved_register(
cpu_architecture: &str,
) -> (Vec<String>, Vec<String>) {
let project_dirs = directories::ProjectDirs::from("", "", "cwe_checker")
.expect("Could not discern location of configuration files.");
let config_dir = project_dirs.config_dir();
let register_config_path = config_dir.join("registers.json");
let file = std::fs::read_to_string(register_config_path)
.expect("Could not read register configuration file");
let mut registers_json: serde_json::Value = serde_json::from_str(&file).unwrap();
match cpu_architecture {
"x86" | "x86_32" => registers_json = registers_json["elf"]["x86"]["cdecl"].clone(),
_ => registers_json = registers_json["elf"][cpu_architecture].clone(),
}
let mut callee_saved: Vec<String> =
serde_json::from_value(registers_json["callee_saved"].clone()).unwrap();
let mut callee_saved_float: Vec<String> =
serde_json::from_value(registers_json["float_callee_saved"].clone()).unwrap();
callee_saved.append(&mut callee_saved_float);
let mut params: Vec<String> = serde_json::from_value(registers_json["params"].clone()).unwrap();
let mut params_float: Vec<String> =
serde_json::from_value(registers_json["float_params"].clone()).unwrap();
params.append(&mut params_float);
(params, callee_saved)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment