Skip to content

Commit 4838f54

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 4838f54

File tree

7 files changed

+294
-41
lines changed

7 files changed

+294
-41
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
enum tagt
22+
{
23+
CHAR,
24+
INT,
25+
CHAR_PTR,
26+
INT_ARR,
27+
STRUCT_ST1_ARR
28+
};
29+
30+
// clang-format off
31+
struct taggedt {
32+
enum tagt tag;
33+
union {
34+
struct{ char a; };
35+
struct{ int b; };
36+
struct{ char *ptr; };
37+
struct{ int arr[10]; };
38+
struct{ struct st1 arr1[10]; };
39+
};
40+
};
41+
// clang-format on
42+
43+
int foo(int i) __CPROVER_assigns()
44+
{
45+
// all accesses to locals should pass
46+
int arr[10];
47+
struct st1 s1;
48+
struct st2 s2;
49+
struct st1 dirty_s1;
50+
struct st3 s3;
51+
s3.s1 = &dirty_s1;
52+
s3.s2 = malloc(sizeof(struct st2));
53+
54+
if(0 <= i && i < 10)
55+
{
56+
arr[i] = 0;
57+
s1.a = 0;
58+
s1.arr[i] = 0;
59+
s2.a = 0;
60+
s2.arr[i].a = 0;
61+
s2.arr[i].arr[i] = 0;
62+
s3.s1->a = 0;
63+
s3.s1->arr[i] = 0;
64+
s3.s2->a = 0;
65+
s3.s2->arr[i].a = 0;
66+
s3.s2->arr[i].arr[i] = 0;
67+
*(&(s3.s2->arr[i].arr[0]) + i) = 0;
68+
(&(s3.arr1[0]) + i)->arr[i] = 0;
69+
(&((&(s3.arr2[0]) + i)->arr[i]))->a = 0;
70+
}
71+
72+
struct taggedt tagged;
73+
switch(tagged.tag)
74+
{
75+
case CHAR:
76+
tagged.a = 0;
77+
break;
78+
case INT:
79+
tagged.b = 0;
80+
break;
81+
case CHAR_PTR:
82+
tagged.ptr = 0;
83+
break;
84+
case INT_ARR:
85+
if(0 <= i && i < 10)
86+
tagged.arr[i] = 0;
87+
break;
88+
case STRUCT_ST1_ARR:
89+
if(0 <= i && i < 10)
90+
{
91+
tagged.arr1[i].a = 0;
92+
tagged.arr1[i].arr[i] = 0;
93+
}
94+
break;
95+
}
96+
97+
return 0;
98+
}
99+
100+
void main()
101+
{
102+
int i;
103+
foo(i);
104+
}
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.

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: 99 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,107 @@ 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.
858+
// Since non-dirty locals are not tracked explicitly in the write set,
859+
// we need to skip the check if we can verify that the expression describes
860+
// an access to a non-dirty local symbol or an input parameter,
861+
// otherwise the check will fail.
862+
// In addition, the expression shall not contain address_of or dereference
863+
// operators, regardless of the base symbol/object on which they apply.
864+
// If the expression contains an address_of operation, the assignment gets
865+
// checked. If the base object is a local or a parameter, it will also be
866+
// flagged as dirty and will be tracked explicitly, and the check will pass.
867+
// If the expression contains a dereference operation, the assignment gets
868+
// checked. If the dereferenced address was computed from a local object,
869+
// from a function parameter or returned by a local malloc,
870+
// then the object will be tracked explicitly and the check will pass.
871+
// In all other cases (address of a non-local object, or dereference of
872+
// a non-locally computed address) the location must be given explicitly
873+
// in the assigns clause to be tracked and we must check the assignment.
874+
if(
875+
cfg_info_opt.has_value() &&
876+
cfg_info_opt.value().is_local_composite_access(target->assign_lhs()))
877+
{
878+
log.debug()
879+
<< LOG_HEADER
880+
<< "skipping check on assignment to local composite member expression "
881+
<< format(target->assign_lhs()) << messaget::eom;
882+
return false;
883+
}
884+
log.debug() << LOG_HEADER << "checking assignment to expression "
885+
<< format(target->assign_lhs()) << messaget::eom;
886+
return true;
822887
}
888+
}
823889

824-
return true;
890+
/// Track the symbol iff we have no cfg_infot, or we have a cfg_infot and the
891+
/// symbol is not a local or is a dirty local.
892+
bool instrument_spec_assignst::must_track_decl_or_dead(
893+
const irep_idt &ident,
894+
const optionalt<cfg_infot> &cfg_info_opt) const
895+
{
896+
return !cfg_info_opt.has_value() ||
897+
(cfg_info_opt.has_value() &&
898+
cfg_info_opt.value().is_not_local_or_dirty_local(ident));
825899
}
826900

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.
901+
/// Returns true iff a `DECL x` must be explicitly tracked in the write set.
836902
bool instrument_spec_assignst::must_track_decl(
837903
const goto_programt::const_targett &target,
838904
const optionalt<cfg_infot> &cfg_info_opt) const
839905
{
840-
if(cfg_info_opt.has_value())
906+
log.debug().source_location = target->source_location();
907+
if(must_track_decl_or_dead(
908+
target->decl_symbol().get_identifier(), cfg_info_opt))
841909
{
842-
return cfg_info_opt.value().is_not_local_or_dirty_local(
843-
target->decl_symbol().get_identifier());
910+
log.debug() << LOG_HEADER << "explicitly tracking "
911+
<< format(target->decl_symbol()) << " as assignable"
912+
<< messaget::eom;
913+
return true;
914+
}
915+
else
916+
{
917+
log.debug() << LOG_HEADER << "implicitly tracking "
918+
<< format(target->decl_symbol())
919+
<< " as assignable (non-dirty local)" << messaget::eom;
920+
return false;
844921
}
845-
// Unless proved non-dirty by the CFG analysis we assume it is dirty.
846-
return true;
847922
}
848923

849924
/// Returns true iff a `DEAD x` must be processed to upate the local write set.
@@ -852,12 +927,8 @@ bool instrument_spec_assignst::must_track_dead(
852927
const goto_programt::const_targett &target,
853928
const optionalt<cfg_infot> &cfg_info_opt) const
854929
{
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());
930+
return must_track_decl_or_dead(
931+
target->dead_symbol().get_identifier(), cfg_info_opt);
861932
}
862933

863934
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,

0 commit comments

Comments
 (0)