Skip to content

Commit 4bc8724

Browse files
author
Remi Delmas
committed
CONTRACTS: handle locals with composite types when checking assigns clauses
We now detect assignments to members of local symbols that have composite types, and skip checking them (i.e. they are implicitly allowed). OOB accesses to local arrays or structs must be detected using pointer checks. Before, these would cause assigns clause checking to systematically fail. Added debug output prints to give users insight into which targets are tracked, which local statics are automatically included in the write set, which assignments are checked or skipped.
1 parent 94b81b2 commit 4bc8724

File tree

9 files changed

+243
-41
lines changed

9 files changed

+243
-41
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
struct st1
2+
{
3+
int a;
4+
int arr[10];
5+
};
6+
7+
struct st2
8+
{
9+
int a;
10+
struct st1 arr[10];
11+
};
12+
13+
struct st3
14+
{
15+
struct st1 *s1;
16+
struct st2 *s2;
17+
struct st1 arr1[10];
18+
struct st2 arr2[10];
19+
};
20+
21+
int foo(int i) __CPROVER_assigns()
22+
{
23+
int arr[10];
24+
struct st1 s1;
25+
struct st2 s2;
26+
struct st3 s3;
27+
s3.s1 = &s1;
28+
s3.s2 = &s2;
29+
if(0 <= i && i < 10)
30+
{
31+
arr[i] = 0;
32+
s1.a = 0;
33+
s1.arr[i] = 0;
34+
s2.a = 0;
35+
s2.arr[i].a = 0;
36+
s2.arr[i].arr[i] = 0;
37+
s3.s1->a = 0;
38+
s3.s1->arr[i] = 0;
39+
s3.s2->a = 0;
40+
s3.s2->arr[i].a = 0;
41+
s3.s2->arr[i].arr[i] = 0;
42+
*(&(s3.s2->arr[i].arr[0]) + i) = 0;
43+
(&(s3.arr1[0]) + i)->arr[i] = 0;
44+
(&((&(s3.arr2[0]) + i)->arr[i]))->a = 0;
45+
}
46+
47+
return 0;
48+
}
49+
50+
void main()
51+
{
52+
int i;
53+
foo(i);
54+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE
2+
main.c
3+
--enforce-contract foo
4+
^EXIT=0$
5+
^SIGNAL=0$
6+
^VERIFICATION SUCCESSFUL$
7+
--
8+
--
9+
Checks that assigns clause checking explicitly checks assignments to locally
10+
declared symbols with composite types, when they are dirty.
11+
Out of bounds accesses to locally declared arrays, structs, etc.
12+
will be detected by assigns clause checking.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
struct st1
2+
{
3+
int a;
4+
int arr[10];
5+
};
6+
7+
struct st2
8+
{
9+
int a;
10+
struct st1 arr[10];
11+
};
12+
13+
int foo(int i) __CPROVER_assigns()
14+
{
15+
int arr[10];
16+
struct st1 s1;
17+
struct st2 s2;
18+
if(0 <= i && i < 10)
19+
{
20+
arr[i] = 0;
21+
s1.a = 0;
22+
s1.arr[i] = 0;
23+
s2.a = 0;
24+
s2.arr[i].a = 0;
25+
s2.arr[i].arr[i] = 0;
26+
}
27+
28+
return 0;
29+
}
30+
31+
void main()
32+
{
33+
int i;
34+
foo(i);
35+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE
2+
main.c
3+
--enforce-contract foo
4+
^EXIT=0$
5+
^SIGNAL=0$
6+
^VERIFICATION SUCCESSFUL$
7+
--
8+
--
9+
Checks that assigns clause checking implicitly allows assignment to locally
10+
declared symbols with composite types, when they are not dirty.
11+
Out of bounds accesses to locally declared arrays, structs, etc. Should be
12+
detected using pointer checks.

regression/contracts/assigns_type_checking_valid_cases/main.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ void foo4(int a, int *b, int *c) __CPROVER_requires(c != NULL)
4141

4242
void foo5(struct buf buffer) __CPROVER_assigns()
4343
{
44+
// these are assignments to the function parameter which is a local symbol
45+
// and should not generate checks
4446
buffer.data = NULL;
4547
buffer.len = 0;
4648
}

regression/contracts/assigns_type_checking_valid_cases/test.desc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ main.c
88
^\[foo3.assigns.\d+\] line \d+ Check that y is assignable: SUCCESS$
99
^\[foo4.assigns.\d+\] line \d+ Check that \*c is assignable: SUCCESS$
1010
^\[foo4.assigns.\d+\] line \d+ Check that \*x is assignable: SUCCESS$
11-
^\[foo5.assigns.\d+\] line \d+ Check that buffer.data is assignable: SUCCESS$
12-
^\[foo5.assigns.\d+\] line \d+ Check that buffer.len is assignable: SUCCESS$
1311
^\[foo6.assigns.\d+\] line \d+ Check that \*buffer->data is assignable: SUCCESS$
1412
^\[foo6.assigns.\d+\] line \d+ Check that buffer->len is assignable: SUCCESS$
1513
^\[foo7.assigns.\d+\] line \d+ Check that \*buffer->data is assignable: SUCCESS$

src/goto-instrument/contracts/instrument_spec_assigns.cpp

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Date: January 2022
2525

2626
#include "utils.h"
2727

28+
/// header for log messages
29+
static const char LOG_HEADER[] = "assigns clause checking: ";
30+
2831
/// Pragma used to mark assignments to static locals that need to be propagated
2932
static const char PROPAGATE_STATIC_LOCAL_PRAGMA[] =
3033
"contracts:propagate-static-local";
@@ -691,8 +694,9 @@ exprt instrument_spec_assignst::inclusion_check_full(
691694
}
692695

693696
if(allow_null_lhs)
694-
return or_exprt{null_pointer(car.target_start_address()),
695-
and_exprt{car.valid_var(), disjunction(disjuncts)}};
697+
return or_exprt{
698+
null_pointer(car.target_start_address()),
699+
and_exprt{car.valid_var(), disjunction(disjuncts)}};
696700
else
697701
return and_exprt{car.valid_var(), disjunction(disjuncts)};
698702
}
@@ -713,6 +717,8 @@ const car_exprt &instrument_spec_assignst::create_car_from_spec_assigns(
713717
}
714718
else
715719
{
720+
log.debug() << LOG_HEADER << "creating CAR for assigns clause target "
721+
<< format(condition) << ": " << format(target) << messaget::eom;
716722
from_spec_assigns.insert({key, create_car_expr(condition, target)});
717723
return from_spec_assigns.find(key)->second;
718724
}
@@ -731,6 +737,8 @@ const car_exprt &instrument_spec_assignst::create_car_from_stack_alloc(
731737
}
732738
else
733739
{
740+
log.debug() << LOG_HEADER << "creating CAR for stack-allocated target "
741+
<< format(target) << messaget::eom;
734742
from_stack_alloc.insert({target, create_car_expr(true_exprt{}, target)});
735743
return from_stack_alloc.find(target)->second;
736744
}
@@ -749,6 +757,8 @@ instrument_spec_assignst::create_car_from_heap_alloc(const exprt &target)
749757
}
750758
else
751759
{
760+
log.debug() << LOG_HEADER << "creating CAR for heap-allocated target "
761+
<< format(target) << messaget::eom;
752762
from_heap_alloc.insert({target, create_car_expr(true_exprt{}, target)});
753763
return from_heap_alloc.find(target)->second;
754764
}
@@ -767,6 +777,8 @@ const car_exprt &instrument_spec_assignst::create_car_from_static_local(
767777
}
768778
else
769779
{
780+
log.debug() << LOG_HEADER << "creating CAR for static local target "
781+
<< format(target) << messaget::eom;
770782
from_static_local.insert({target, create_car_expr(true_exprt{}, target)});
771783
return from_static_local.find(target)->second;
772784
}
@@ -806,44 +818,91 @@ bool instrument_spec_assignst::must_check_assign(
806818
skip_function_paramst skip_function_params,
807819
const optionalt<cfg_infot> cfg_info_opt)
808820
{
809-
if(
810-
const auto &symbol_expr =
811-
expr_try_dynamic_cast<symbol_exprt>(target->assign_lhs()))
821+
log.debug().source_location = target->source_location();
822+
823+
if(can_cast_expr<symbol_exprt>(target->assign_lhs()))
812824
{
825+
const auto &symbol_expr = to_symbol_expr(target->assign_lhs());
813826
if(
814827
skip_function_params == skip_function_paramst::NO &&
815-
ns.lookup(symbol_expr->get_identifier()).is_parameter)
828+
ns.lookup(symbol_expr.get_identifier()).is_parameter)
816829
{
830+
log.debug() << LOG_HEADER << "checking assignment to function parameter "
831+
<< format(symbol_expr) << messaget::eom;
817832
return true;
818833
}
819834

820835
if(cfg_info_opt.has_value())
821-
return !cfg_info_opt.value().is_local(symbol_expr->get_identifier());
836+
{
837+
if(cfg_info_opt.value().is_local(symbol_expr.get_identifier()))
838+
{
839+
log.debug() << LOG_HEADER
840+
<< "skipping checking on assignment to local symbol "
841+
<< format(symbol_expr) << messaget::eom;
842+
return false;
843+
}
844+
else
845+
{
846+
log.debug() << LOG_HEADER << "checking assignment to non-local symbol "
847+
<< format(symbol_expr) << messaget::eom;
848+
return true;
849+
}
850+
}
851+
log.debug() << LOG_HEADER << "checking assignment to symbol "
852+
<< format(symbol_expr) << messaget::eom;
853+
return true;
854+
}
855+
else
856+
{
857+
// This is not a mere symbol. We skip the check only if we can verify that
858+
// this is an access to a locally declared symbol
859+
if(
860+
cfg_info_opt.has_value() &&
861+
cfg_info_opt.value().is_access_to_local_composite(target->assign_lhs()))
862+
{
863+
log.debug() << LOG_HEADER
864+
<< "skipping check on assignment to local expression "
865+
<< format(target->assign_lhs()) << messaget::eom;
866+
return false;
867+
}
868+
log.debug() << LOG_HEADER << "checking assignment to expression "
869+
<< format(target->assign_lhs()) << messaget::eom;
870+
return true;
822871
}
872+
}
823873

824-
return true;
874+
/// Track the symbol iff we have no cfg_infot, or we have a cfg_infot and the
875+
/// symbol is not a local or is a dirty local.
876+
bool instrument_spec_assignst::must_track_decl_or_dead(
877+
const irep_idt &ident,
878+
const optionalt<cfg_infot> &cfg_info_opt) const
879+
{
880+
return !cfg_info_opt.has_value() ||
881+
(cfg_info_opt.has_value() &&
882+
cfg_info_opt.value().is_not_local_or_dirty_local(ident));
825883
}
826884

827-
/// Returns true iff a `DECL x` must be added to the local write set.
828-
///
829-
/// A variable is called 'dirty' if its address gets taken at some point in
830-
/// the program.
831-
///
832-
/// Assuming the goto program is obtained from a structured C program that
833-
/// passed C compiler checks, non-dirty variables can only be assigned to
834-
/// directly by name, cannot escape their lexical scope, and are always safe
835-
/// to assign. Hence, we only track dirty variables in the write set.
885+
/// Returns true iff a `DECL x` must be explicitly tracked in the write set.
836886
bool instrument_spec_assignst::must_track_decl(
837887
const goto_programt::const_targett &target,
838888
const optionalt<cfg_infot> &cfg_info_opt) const
839889
{
840-
if(cfg_info_opt.has_value())
890+
log.debug().source_location = target->source_location();
891+
if(must_track_decl_or_dead(
892+
target->decl_symbol().get_identifier(), cfg_info_opt))
841893
{
842-
return cfg_info_opt.value().is_not_local_or_dirty_local(
843-
target->decl_symbol().get_identifier());
894+
log.debug() << LOG_HEADER << "explicitly tracking "
895+
<< format(target->decl_symbol()) << " as assignable"
896+
<< messaget::eom;
897+
return true;
898+
}
899+
else
900+
{
901+
log.debug() << LOG_HEADER << "implicitly tracking "
902+
<< format(target->decl_symbol())
903+
<< " as assignable (non-dirty local)" << messaget::eom;
904+
return false;
844905
}
845-
// Unless proved non-dirty by the CFG analysis we assume it is dirty.
846-
return true;
847906
}
848907

849908
/// Returns true iff a `DEAD x` must be processed to upate the local write set.
@@ -852,12 +911,8 @@ bool instrument_spec_assignst::must_track_dead(
852911
const goto_programt::const_targett &target,
853912
const optionalt<cfg_infot> &cfg_info_opt) const
854913
{
855-
// Unless proved non-dirty by the CFG analysis we assume it is dirty.
856-
if(!cfg_info_opt.has_value())
857-
return true;
858-
859-
return cfg_info_opt.value().is_not_local_or_dirty_local(
860-
target->dead_symbol().get_identifier());
914+
return must_track_decl_or_dead(
915+
target->dead_symbol().get_identifier(), cfg_info_opt);
861916
}
862917

863918
void instrument_spec_assignst::instrument_assign_statement(

src/goto-instrument/contracts/instrument_spec_assigns.h

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -561,25 +561,31 @@ class instrument_spec_assignst
561561
const car_exprt &freed_car,
562562
goto_programt &dest) const;
563563

564-
/// Returns true iff a `DECL x` must be added to the local write set.
565-
///
566-
/// A variable is called 'dirty' if its address gets taken at some point in
567-
/// the program.
568-
///
569-
/// Assuming the goto program is obtained from a structured C program that
570-
/// passed C compiler checks, non-dirty variables can only be assigned to
571-
/// directly by name, cannot escape their lexical scope, and are always safe
572-
/// to assign. Hence, we only track dirty variables in the write set.
564+
/// Returns true iff a `DECL x` must be explicitly added to the write set.
565+
/// @see must_track_decl_or_dead.
573566
bool must_track_decl(
574567
const goto_programt::const_targett &target,
575568
const optionalt<cfg_infot> &cfg_info_opt) const;
576569

577-
/// Returns true iff a `DEAD x` must be processed to update the local write
578-
/// set. The conditions are the same than for tracking a `DECL x` instruction.
570+
/// Returns true iff a `DEAD x` must be processed to update the write set.
571+
/// @see must_track_decl_or_dead.
579572
bool must_track_dead(
580573
const goto_programt::const_targett &target,
581574
const optionalt<cfg_infot> &cfg_info_opt) const;
582575

576+
/// \brief Returns true iff a function-local symbol must be tracked.
577+
///
578+
/// A local is called 'dirty' if its address gets taken at some point in
579+
/// the program.
580+
///
581+
/// Assuming the goto program is obtained from a structured C program that
582+
/// passed C compiler checks, non-dirty locals can only be assigned to
583+
/// directly by name, cannot escape their lexical scope, and are always safe
584+
/// to assign. Hence, we only track dirty locals in the write set.
585+
bool must_track_decl_or_dead(
586+
const irep_idt &ident,
587+
const optionalt<cfg_infot> &cfg_info_opt) const;
588+
583589
/// Returns true iff an `ASSIGN lhs := rhs` instruction must be instrumented.
584590
bool must_check_assign(
585591
const goto_programt::const_targett &target,

src/goto-instrument/contracts/utils.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,34 @@ class cfg_infot
190190
return locals.is_local(ident) || symbol.is_parameter;
191191
}
192192

193+
/// Returns true iff `expr` is a composite type access on a locally declared
194+
/// or parameter symbol, without any dereference operations.
195+
bool is_access_to_local_composite(const exprt &expr) const
196+
{
197+
if(expr.id() == ID_symbol)
198+
{
199+
return is_local(to_symbol_expr(expr).get_identifier());
200+
}
201+
else if(expr.id() == ID_index)
202+
{
203+
return is_access_to_local_composite(to_index_expr(expr).array());
204+
}
205+
else if(expr.id() == ID_member)
206+
{
207+
return is_access_to_local_composite(to_member_expr(expr).struct_op());
208+
}
209+
else if(expr.id() == ID_if)
210+
{
211+
return is_access_to_local_composite(to_if_expr(expr).true_case()) &&
212+
is_access_to_local_composite(to_if_expr(expr).false_case());
213+
}
214+
else
215+
{
216+
// matches ID_dereference
217+
return false;
218+
}
219+
}
220+
193221
/// returns the current target instruction
194222
const goto_programt::targett &get_current_target() const
195223
{

0 commit comments

Comments
 (0)