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
2808701c
Unverified
Commit
2808701c
authored
May 05, 2022
by
van den Bosch
Committed by
GitHub
May 05, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Stack alignment analysis (#317)
parent
2c469338
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
568 additions
and
1 deletions
+568
-1
mod.rs
src/cwe_checker_lib/src/analysis/mod.rs
+1
-0
mod.rs
...cker_lib/src/analysis/stack_alignment_substitution/mod.rs
+175
-0
tests.rs
...er_lib/src/analysis/stack_alignment_substitution/tests.rs
+384
-0
blk.rs
src/cwe_checker_lib/src/intermediate_representation/blk.rs
+1
-0
project.rs
...we_checker_lib/src/intermediate_representation/project.rs
+7
-1
No files found.
src/cwe_checker_lib/src/analysis/mod.rs
View file @
2808701c
...
...
@@ -9,5 +9,6 @@ pub mod function_signature;
pub
mod
graph
;
pub
mod
interprocedural_fixpoint_generic
;
pub
mod
pointer_inference
;
pub
mod
stack_alignment_substitution
;
pub
mod
string_abstraction
;
pub
mod
vsa_results
;
src/cwe_checker_lib/src/analysis/stack_alignment_substitution/mod.rs
0 → 100644
View file @
2808701c
//! Substitutes stack pointer alignment operations utilising logical AND with an arithmetic SUB operation.
//!
//! The first basic block of every function is searched for a logical AND operation on the stack pointer.
//! By journaling changes to the stack pointer an offset is calculated which is going to be used to alter the operation
//! into a subtraction.
//!
//! # Log Messages
//! Following cases trigger log messages:
//! - alignment is untypical for the architecture
//! - the argument for the AND operation is not a constant
//! - an operation alters the stack pointer, which can not be journaled.
use
anyhow
::{
anyhow
,
Result
};
use
apint
::
ApInt
;
use
itertools
::
Itertools
;
use
crate
::{
intermediate_representation
::
*
,
utils
::
log
::
LogMessage
};
/// Substitutes AND operation by SUB operation with calculated constants.
/// Constants are derived by a journaled stackpointer value and the bitmask provided by the operation.
fn
substitute
(
exp
:
&
mut
Expression
,
expected_alignment
:
i64
,
journaled_sp
:
&
mut
i64
,
tid
:
Tid
,
)
->
Vec
<
LogMessage
>
{
let
mut
log
:
Vec
<
LogMessage
>
=
vec!
[];
if
let
Expression
::
BinOp
{
op
,
lhs
,
rhs
}
=
exp
{
match
(
&**
lhs
,
&**
rhs
)
{
(
Expression
::
Var
(
sp
),
Expression
::
Const
(
bitmask
))
|
(
Expression
::
Const
(
bitmask
),
Expression
::
Var
(
sp
))
=>
{
if
let
BinOpType
::
IntAnd
=
op
{
if
ApInt
::
try_to_i64
(
&
ApInt
::
into_negate
(
bitmask
.clone
()))
.unwrap
()
!=
expected_alignment
{
log
.push
(
LogMessage
::
new_info
(
"Unexpected alignment"
)
.location
(
tid
));
}
let
offset
=
*
journaled_sp
-
(
*
journaled_sp
&
bitmask
.clone
()
.try_to_i64
()
.unwrap
());
let
sp
=
sp
.clone
();
*
op
=
BinOpType
::
IntSub
;
*
rhs
=
Box
::
new
(
Expression
::
Const
(
(
ApInt
::
from_i64
(
offset
))
.into_resize_unsigned
(
bitmask
.bytesize
()),
));
*
lhs
=
Box
::
new
(
Expression
::
Var
(
sp
));
}
else
{
log
.push
(
LogMessage
::
new_info
(
"Unsubstitutable Operation on SP"
)
.location
(
tid
))
};
}
_
=>
log
.push
(
LogMessage
::
new_info
(
"Unsubstitutable Operation on SP. Operants are not register and constant."
,
)
.location
(
tid
),
),
}
}
else
{
log
.push
(
LogMessage
::
new_info
(
"Unsubstitutable Operation on SP"
)
.location
(
tid
))
}
log
}
/// Updates current stackpointer value by given Constant.
fn
journal_sp_value
(
journaled_sp
:
&
mut
i64
,
is_plus
:
bool
,
(
rhs
,
lhs
):
(
&
Expression
,
&
Expression
),
sp_register
:
&
Variable
,
)
->
Result
<
()
>
{
match
(
rhs
,
lhs
)
{
(
Expression
::
Var
(
sp
),
Expression
::
Const
(
constant
))
|
(
Expression
::
Const
(
constant
),
Expression
::
Var
(
sp
))
=>
{
if
sp
==
sp_register
{
match
is_plus
{
true
=>
*
journaled_sp
+=
constant
.try_to_i64
()
.unwrap
(),
false
=>
*
journaled_sp
-
=
constant
.try_to_i64
()
.unwrap
(),
}
Ok
(())
}
else
{
Err
(
anyhow!
(
"Input not stackpointer register and constant."
))
}
}
_
=>
Err
(
anyhow!
(
"Input not register and constant."
)),
}
}
/// Substitutes logical AND on the stackpointer register by SUB.
/// Expressions are changed to use constants w.r.t the provided bit mask.
pub
fn
substitute_and_on_stackpointer
(
project
:
&
mut
Project
)
->
Option
<
Vec
<
LogMessage
>>
{
// for sanity check
let
sp_alignment
=
match
project
.cpu_architecture
.as_str
()
{
"x86_32"
=>
16
,
"x86_64"
=>
16
,
"arm32"
=>
4
,
_
=>
0
,
};
let
mut
log
:
Vec
<
LogMessage
>
=
vec!
[];
'sub_loop
:
for
sub
in
project
.program.term.subs
.values_mut
()
{
let
journaled_sp
:
&
mut
i64
=
&
mut
0
;
// only for the first block SP can be reasonable tracked
if
let
Some
(
blk
)
=
sub
.term.blocks
.first_mut
()
{
for
def
in
blk
.term.defs
.iter_mut
()
{
if
let
Def
::
Assign
{
var
,
value
}
=
&
mut
def
.term
{
if
*
var
==
project
.stack_pointer_register
{
if
let
Expression
::
BinOp
{
op
,
lhs
,
rhs
}
=
value
{
match
op
{
BinOpType
::
IntAdd
=>
{
if
journal_sp_value
(
journaled_sp
,
true
,
(
lhs
,
rhs
),
&
project
.stack_pointer_register
,
)
.is_err
()
{
continue
'sub_loop
;
}
}
BinOpType
::
IntSub
=>
{
if
journal_sp_value
(
journaled_sp
,
false
,
(
lhs
,
rhs
),
&
project
.stack_pointer_register
,
)
.is_err
()
{
continue
'sub_loop
;
}
}
_
=>
{
let
mut
msg
=
substitute
(
value
,
sp_alignment
,
journaled_sp
,
def
.tid
.clone
(),
);
log
.append
(
&
mut
msg
);
if
!
log
.iter
()
.filter
(|
x
|
{
x
.text
.contains
(
"Unsubstitutable Operation on SP"
)
})
.collect_vec
()
.is_empty
()
{
// Lost track of SP
continue
'sub_loop
;
}
}
}
}
else
{
log
.push
(
LogMessage
::
new_info
(
"Unexpected assignment on SP"
)
.location
(
def
.tid
.clone
()),
);
continue
'sub_loop
;
}
}
}
}
}
}
if
log
.is_empty
()
{
return
None
;
}
Some
(
log
)
}
#[cfg(test)]
mod
tests
;
src/cwe_checker_lib/src/analysis/stack_alignment_substitution/tests.rs
0 → 100644
View file @
2808701c
use
super
::
*
;
use
std
::
borrow
::
BorrowMut
;
/// Creates a x64 or ARM32 Project for easy addidion of assignments.
fn
setup
(
mut
defs
:
Vec
<
Term
<
Def
>>
,
is_x64
:
bool
)
->
Project
{
let
mut
proj
=
match
is_x64
{
true
=>
Project
::
mock_x64
(),
false
=>
Project
::
mock_arm32
(),
};
let
mut
blk
=
Blk
::
mock
();
blk
.term.defs
.append
(
defs
.as_mut
());
let
mut
sub
=
Sub
::
mock
(
"Sub"
);
sub
.term.blocks
.push
(
blk
);
proj
.program.term.subs
.insert
(
Tid
::
new
(
"sub_tid"
),
sub
);
proj
}
#[test]
/// Tests the return of log messages for all alignments, including unexpected alignments for x64 and arm32.
fn
unexpected_alignment
()
{
for
i
in
0
..
31
{
// case x64
let
def_x64
=
vec!
[
Def
::
assign
(
"tid1"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u32
(
0xFFFFFFFF
<<
i
,
))),
},
)];
let
mut
proj_x64
=
setup
(
def_x64
,
true
);
let
log
=
substitute_and_on_stackpointer
(
proj_x64
.borrow_mut
());
if
2
_i32
.pow
(
i
)
==
16
{
assert
!
(
log
.is_none
());
}
else
{
assert
!
(
log
.is_some
());
for
msg
in
log
.unwrap
()
{
assert
!
(
msg
.text
.contains
(
"Unexpected alignment"
));
}
}
// case ARM32
let
def_arm
=
vec!
[
Def
::
assign
(
"tid1"
,
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u32
(
0xFFFFFFFF
<<
i
,
))),
},
)];
let
mut
proj_arm
=
setup
(
def_arm
,
false
);
let
log
=
substitute_and_on_stackpointer
(
proj_arm
.borrow_mut
());
if
2
_i32
.pow
(
i
)
==
4
{
assert
!
(
log
.is_none
());
}
else
{
assert
!
(
log
.is_some
());
for
msg
in
log
.unwrap
()
{
assert
!
(
msg
.text
.contains
(
"Unexpected alignment"
));
}
}
}
}
#[test]
/// Tests the substituted offset meets the alignment for x64. Tests only the logical AND case.
fn
compute_correct_offset_x64
()
{
for
i
in
0
..=
33
{
let
sub_from_sp
=
Def
::
assign
(
"tid_alter_sp"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
minus
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
()),
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
i
)),
),
);
let
byte_alignment_as_and
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
0xFFFFFFFF
_FFFFFFFF
<<
4
,
// 16 Byte alignment
))),
},
);
let
mut
proj
=
setup
(
vec!
[
sub_from_sp
.clone
(),
byte_alignment_as_and
.clone
()],
true
,
);
let
log
=
substitute_and_on_stackpointer
(
proj
.borrow_mut
());
for
sub
in
proj
.program.term.subs
.into_values
()
{
for
blk
in
sub
.term.blocks
{
for
def
in
blk
.term.defs
{
if
def
.tid
==
byte_alignment_as_and
.tid
.clone
()
{
let
expected_offset
:
u64
=
match
i
%
16
{
0
=>
0
,
_
=>
(
16
-
(
i
%
16
))
.into
(),
};
// translated alignment as substraction
let
expected_def
=
Def
::
Assign
{
var
:
proj
.stack_pointer_register
.clone
(),
value
:
Expression
::
BinOp
{
op
:
BinOpType
::
IntSub
,
lhs
:
Box
::
new
(
Expression
::
Var
(
proj
.stack_pointer_register
.clone
())),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
expected_offset
,
))),
},
};
assert_eq!
(
expected_def
,
def
.term
);
assert
!
(
log
.is_none
());
}
}
}
}
}
}
#[test]
/// Tests the substituted offset meets the alignment for arm32. Tests only the logical AND case.
fn
compute_correct_offset_arm32
()
{
for
i
in
0
..=
33
{
let
sub_from_sp
=
Def
::
assign
(
"tid_alter_sp"
,
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
Expression
::
minus
(
Expression
::
Var
(
Project
::
mock_arm32
()
.stack_pointer_register
.clone
()),
Expression
::
const_from_apint
(
ApInt
::
from_u32
(
i
)),
),
);
let
byte_alignment_as_and
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u32
(
0xFFFFFFFF
<<
2
,
// 4 Byte alignment
))),
},
);
let
mut
proj
=
setup
(
vec!
[
sub_from_sp
.clone
(),
byte_alignment_as_and
.clone
()],
false
,
);
let
log
=
substitute_and_on_stackpointer
(
proj
.borrow_mut
());
for
sub
in
proj
.program.term.subs
.into_values
()
{
for
blk
in
sub
.term.blocks
{
for
def
in
blk
.term.defs
{
if
def
.tid
==
byte_alignment_as_and
.tid
.clone
()
{
let
expected_offset
=
match
i
%
4
{
0
=>
0
,
_
=>
4
-
(
i
%
4
),
};
// translated alignment as substraction
let
expected_def
=
Def
::
Assign
{
var
:
proj
.stack_pointer_register
.clone
(),
value
:
Expression
::
BinOp
{
op
:
BinOpType
::
IntSub
,
lhs
:
Box
::
new
(
Expression
::
Var
(
proj
.stack_pointer_register
.clone
())),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u32
(
expected_offset
,
))),
},
};
assert_eq!
(
expected_def
,
def
.term
);
assert
!
(
log
.is_none
());
}
}
}
}
}
}
#[test]
/// Checks behaviour on supported and unsupported binary operations.
fn
check_bin_operations
()
{
for
biopty
in
vec!
[
BinOpType
::
Piece
,
BinOpType
::
IntAdd
,
BinOpType
::
IntSub
,
BinOpType
::
IntAnd
,
BinOpType
::
IntOr
,
]
{
let
unsupported_def_x64
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
biopty
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_i32
(
0
)),
},
);
let
unsupported_def_arm32
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
biopty
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_arm32
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_i32
(
0
)),
},
);
let
mut
proj_x64
=
setup
(
vec!
[
unsupported_def_x64
.clone
()],
true
);
let
log_x64
=
substitute_and_on_stackpointer
(
proj_x64
.borrow_mut
());
let
mut
proj_arm32
=
setup
(
vec!
[
unsupported_def_arm32
.clone
()],
false
);
let
log_arm32
=
substitute_and_on_stackpointer
(
proj_arm32
.borrow_mut
());
for
log
in
vec!
[
log_arm32
,
log_x64
]
{
match
biopty
{
BinOpType
::
IntAnd
=>
{
assert_eq!
(
log
.clone
()
.unwrap
()
.len
(),
1
);
assert
!
(
log
.unwrap
()
.pop
()
.unwrap
()
.text
.contains
(
"Unexpected alignment"
));
}
BinOpType
::
IntAdd
|
BinOpType
::
IntSub
=>
{
assert
!
(
log
.is_none
())
}
_
=>
{
assert_eq!
(
log
.clone
()
.unwrap
()
.len
(),
1
);
assert
!
(
log
.unwrap
()
.pop
()
.unwrap
()
.text
.contains
(
"Unsubstitutable Operation on SP"
));
}
}
}
}
}
#[test]
/// Checks if the substitution on logical operations ends if an unsubstitutable operation occured.
fn
substitution_ends_if_unsubstituable
()
{
let
alignment_16_byte_as_and
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
0xFFFFFFFF
_FFFFFFFF
<<
4
,
// 16 Byte alignment
))),
},
);
let
unsubstitutable
=
Def
::
assign
(
"tid_unsubstitutable"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
Piece
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_i64
(
0
)),
},
);
let
mut
proj
=
setup
(
vec!
[
alignment_16_byte_as_and
.clone
(),
unsubstitutable
.clone
(),
alignment_16_byte_as_and
.clone
(),
],
true
,
);
let
log
=
substitute_and_on_stackpointer
(
proj
.borrow_mut
());
assert
!
(
log
.is_some
());
assert
!
(
log
.unwrap
()
.pop
()
.unwrap
()
.text
.contains
(
"Unsubstitutable Operation on SP"
));
let
exp_16_byte_alignment_substituted
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntSub
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
0
))),
},
);
for
sub
in
proj
.program.term.subs
.into_values
()
{
for
blk
in
sub
.term.blocks
{
assert_eq!
(
blk
.term.defs
,
vec!
[
exp_16_byte_alignment_substituted
.clone
(),
unsubstitutable
.clone
(),
alignment_16_byte_as_and
.clone
()
]
);
}
}
}
#[test]
/// Tests if the substitution supports commutativity of the expression.
fn
supports_commutative_and
()
{
let
var_and_bitmask
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
0xFFFFFFFF
_FFFFFFFF
<<
4
,
// 16 Byte alignment
))),
},
);
let
bitmask_and_var
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntAnd
,
lhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
0xFFFFFFFF
_FFFFFFFF
<<
4
,
// 16 Byte alignment
))),
rhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
},
);
let
mut
proj
=
setup
(
vec!
[
bitmask_and_var
],
true
);
let
log
=
substitute_and_on_stackpointer
(
proj
.borrow_mut
());
assert
!
(
log
.is_none
());
let
expected_def
=
Def
::
assign
(
"tid_to_be_substituted"
,
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
Expression
::
BinOp
{
op
:
BinOpType
::
IntSub
,
lhs
:
Box
::
new
(
Expression
::
Var
(
Project
::
mock_x64
()
.stack_pointer_register
.clone
(),
)),
rhs
:
Box
::
new
(
Expression
::
const_from_apint
(
ApInt
::
from_u64
(
0
))),
},
);
for
sub
in
proj
.program.term.subs
.into_values
()
{
for
blk
in
sub
.term.blocks
{
for
def
in
blk
.term.defs
{
assert_eq!
(
def
,
expected_def
);
}
}
}
}
src/cwe_checker_lib/src/intermediate_representation/blk.rs
View file @
2808701c
...
...
@@ -194,6 +194,7 @@ mod tests {
use
crate
::
intermediate_representation
::{
Def
,
Expression
,
Variable
};
impl
Blk
{
/// Creates empty block with tid "block".
pub
fn
mock
()
->
Term
<
Blk
>
{
Term
{
tid
:
Tid
::
new
(
"block"
),
...
...
src/cwe_checker_lib/src/intermediate_representation/project.rs
View file @
2808701c
...
...
@@ -227,11 +227,17 @@ impl Project {
/// - Remove dead register assignments
#[must_use]
pub
fn
normalize
(
&
mut
self
)
->
Vec
<
LogMessage
>
{
let
logs
=
self
.remove_references_to_nonexisting_tids_and_retarget_non_returning_calls
();
let
mut
logs
=
self
.remove_references_to_nonexisting_tids_and_retarget_non_returning_calls
();
make_block_to_sub_mapping_unique
(
self
);
self
.propagate_input_expressions
();
self
.substitute_trivial_expressions
();
crate
::
analysis
::
dead_variable_elimination
::
remove_dead_var_assignments
(
self
);
logs
.append
(
crate
::
analysis
::
stack_alignment_substitution
::
substitute_and_on_stackpointer
(
self
)
.unwrap_or_default
()
.as_mut
(),
);
logs
}
}
...
...
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