Skip to content

Commit d0b5fb4

Browse files
author
Remi Delmas
committed
CONTRACTS: handle locals with composite types
Detect and skip checking assignments to members of local symbols that have composite types. Added debug output prints about tracked locations, detected local statics, checked or skipped assignments.
1 parent 1d608d2 commit d0b5fb4

File tree

7 files changed

+296
-41
lines changed

7 files changed

+296
-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";
@@ -694,8 +697,9 @@ exprt instrument_spec_assignst::inclusion_check_full(
694697
}
695698

696699
if(allow_null_lhs)
697-
return or_exprt{null_pointer(car.target_start_address()),
698-
and_exprt{car.valid_var(), disjunction(disjuncts)}};
700+
return or_exprt{
701+
null_pointer(car.target_start_address()),
702+
and_exprt{car.valid_var(), disjunction(disjuncts)}};
699703
else
700704
return and_exprt{car.valid_var(), disjunction(disjuncts)};
701705
}
@@ -716,6 +720,8 @@ const car_exprt &instrument_spec_assignst::create_car_from_spec_assigns(
716720
}
717721
else
718722
{
723+
log.debug() << LOG_HEADER << "creating CAR for assigns clause target "
724+
<< format(condition) << ": " << format(target) << messaget::eom;
719725
from_spec_assigns.insert({key, create_car_expr(condition, target)});
720726
return from_spec_assigns.find(key)->second;
721727
}
@@ -734,6 +740,8 @@ const car_exprt &instrument_spec_assignst::create_car_from_stack_alloc(
734740
}
735741
else
736742
{
743+
log.debug() << LOG_HEADER << "creating CAR for stack-allocated target "
744+
<< format(target) << messaget::eom;
737745
from_stack_alloc.insert({target, create_car_expr(true_exprt{}, target)});
738746
return from_stack_alloc.find(target)->second;
739747
}
@@ -752,6 +760,8 @@ instrument_spec_assignst::create_car_from_heap_alloc(const exprt &target)
752760
}
753761
else
754762
{
763+
log.debug() << LOG_HEADER << "creating CAR for heap-allocated target "
764+
<< format(target) << messaget::eom;
755765
from_heap_alloc.insert({target, create_car_expr(true_exprt{}, target)});
756766
return from_heap_alloc.find(target)->second;
757767
}
@@ -770,6 +780,8 @@ const car_exprt &instrument_spec_assignst::create_car_from_static_local(
770780
}
771781
else
772782
{
783+
log.debug() << LOG_HEADER << "creating CAR for static local target "
784+
<< format(target) << messaget::eom;
773785
from_static_local.insert({target, create_car_expr(true_exprt{}, target)});
774786
return from_static_local.find(target)->second;
775787
}
@@ -809,44 +821,107 @@ bool instrument_spec_assignst::must_check_assign(
809821
skip_function_paramst skip_function_params,
810822
const optionalt<cfg_infot> cfg_info_opt)
811823
{
812-
if(
813-
const auto &symbol_expr =
814-
expr_try_dynamic_cast<symbol_exprt>(target->assign_lhs()))
824+
log.debug().source_location = target->source_location();
825+
826+
if(can_cast_expr<symbol_exprt>(target->assign_lhs()))
815827
{
828+
const auto &symbol_expr = to_symbol_expr(target->assign_lhs());
816829
if(
817830
skip_function_params == skip_function_paramst::NO &&
818-
ns.lookup(symbol_expr->get_identifier()).is_parameter)
831+
ns.lookup(symbol_expr.get_identifier()).is_parameter)
819832
{
833+
log.debug() << LOG_HEADER << "checking assignment to function parameter "
834+
<< format(symbol_expr) << messaget::eom;
820835
return true;
821836
}
822837

823838
if(cfg_info_opt.has_value())
824-
return !cfg_info_opt.value().is_local(symbol_expr->get_identifier());
839+
{
840+
if(cfg_info_opt.value().is_local(symbol_expr.get_identifier()))
841+
{
842+
log.debug() << LOG_HEADER
843+
<< "skipping checking on assignment to local symbol "
844+
<< format(symbol_expr) << messaget::eom;
845+
return false;
846+
}
847+
else
848+
{
849+
log.debug() << LOG_HEADER << "checking assignment to non-local symbol "
850+
<< format(symbol_expr) << messaget::eom;
851+
return true;
852+
}
853+
}
854+
log.debug() << LOG_HEADER << "checking assignment to symbol "
855+
<< format(symbol_expr) << messaget::eom;
856+
return true;
857+
}
858+
else
859+
{
860+
// This is not a mere symbol.
861+
// Since non-dirty locals are not tracked explicitly in the write set,
862+
// we need to skip the check if we can verify that the expression describes
863+
// an access to a non-dirty local symbol or an input parameter,
864+
// otherwise the check will fail.
865+
// In addition, the expression shall not contain address_of or dereference
866+
// operators, regardless of the base symbol/object on which they apply.
867+
// If the expression contains an address_of operation, the assignment gets
868+
// checked. If the base object is a local or a parameter, it will also be
869+
// flagged as dirty and will be tracked explicitly, and the check will pass.
870+
// If the expression contains a dereference operation, the assignment gets
871+
// checked. If the dereferenced address was computed from a local object,
872+
// from a function parameter or returned by a local malloc,
873+
// then the object will be tracked explicitly and the check will pass.
874+
// In all other cases (address of a non-local object, or dereference of
875+
// a non-locally computed address) the location must be given explicitly
876+
// in the assigns clause to be tracked and we must check the assignment.
877+
if(
878+
cfg_info_opt.has_value() &&
879+
cfg_info_opt.value().is_local_composite_access(target->assign_lhs()))
880+
{
881+
log.debug()
882+
<< LOG_HEADER
883+
<< "skipping check on assignment to local composite member expression "
884+
<< format(target->assign_lhs()) << messaget::eom;
885+
return false;
886+
}
887+
log.debug() << LOG_HEADER << "checking assignment to expression "
888+
<< format(target->assign_lhs()) << messaget::eom;
889+
return true;
825890
}
891+
}
826892

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

830-
/// Returns true iff a `DECL x` must be added to the local write set.
831-
///
832-
/// A variable is called 'dirty' if its address gets taken at some point in
833-
/// the program.
834-
///
835-
/// Assuming the goto program is obtained from a structured C program that
836-
/// passed C compiler checks, non-dirty variables can only be assigned to
837-
/// directly by name, cannot escape their lexical scope, and are always safe
838-
/// to assign. Hence, we only track dirty variables in the write set.
904+
/// Returns true iff a `DECL x` must be explicitly tracked in the write set.
839905
bool instrument_spec_assignst::must_track_decl(
840906
const goto_programt::const_targett &target,
841907
const optionalt<cfg_infot> &cfg_info_opt) const
842908
{
843-
if(cfg_info_opt.has_value())
909+
log.debug().source_location = target->source_location();
910+
if(must_track_decl_or_dead(
911+
target->decl_symbol().get_identifier(), cfg_info_opt))
844912
{
845-
return cfg_info_opt.value().is_not_local_or_dirty_local(
846-
target->decl_symbol().get_identifier());
913+
log.debug() << LOG_HEADER << "explicitly tracking "
914+
<< format(target->decl_symbol()) << " as assignable"
915+
<< messaget::eom;
916+
return true;
917+
}
918+
else
919+
{
920+
log.debug() << LOG_HEADER << "implicitly tracking "
921+
<< format(target->decl_symbol())
922+
<< " as assignable (non-dirty local)" << messaget::eom;
923+
return false;
847924
}
848-
// Unless proved non-dirty by the CFG analysis we assume it is dirty.
849-
return true;
850925
}
851926

852927
/// Returns true iff a `DEAD x` must be processed to upate the local write set.
@@ -855,12 +930,8 @@ bool instrument_spec_assignst::must_track_dead(
855930
const goto_programt::const_targett &target,
856931
const optionalt<cfg_infot> &cfg_info_opt) const
857932
{
858-
// Unless proved non-dirty by the CFG analysis we assume it is dirty.
859-
if(!cfg_info_opt.has_value())
860-
return true;
861-
862-
return cfg_info_opt.value().is_not_local_or_dirty_local(
863-
target->dead_symbol().get_identifier());
933+
return must_track_decl_or_dead(
934+
target->dead_symbol().get_identifier(), cfg_info_opt);
864935
}
865936

866937
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
@@ -574,25 +574,31 @@ class instrument_spec_assignst
574574
const car_exprt &freed_car,
575575
goto_programt &dest) const;
576576

577-
/// Returns true iff a `DECL x` must be added to the local write set.
578-
///
579-
/// A variable is called 'dirty' if its address gets taken at some point in
580-
/// the program.
581-
///
582-
/// Assuming the goto program is obtained from a structured C program that
583-
/// passed C compiler checks, non-dirty variables can only be assigned to
584-
/// directly by name, cannot escape their lexical scope, and are always safe
585-
/// to assign. Hence, we only track dirty variables in the write set.
577+
/// Returns true iff a `DECL x` must be explicitly added to the write set.
578+
/// @see must_track_decl_or_dead.
586579
bool must_track_decl(
587580
const goto_programt::const_targett &target,
588581
const optionalt<cfg_infot> &cfg_info_opt) const;
589582

590-
/// Returns true iff a `DEAD x` must be processed to update the local write
591-
/// set. The conditions are the same than for tracking a `DECL x` instruction.
583+
/// Returns true iff a `DEAD x` must be processed to update the write set.
584+
/// @see must_track_decl_or_dead.
592585
bool must_track_dead(
593586
const goto_programt::const_targett &target,
594587
const optionalt<cfg_infot> &cfg_info_opt) const;
595588

589+
/// \brief Returns true iff a function-local symbol must be tracked.
590+
///
591+
/// A local is called 'dirty' if its address gets taken at some point in
592+
/// the program.
593+
///
594+
/// Assuming the goto program is obtained from a structured C program that
595+
/// passed C compiler checks, non-dirty locals can only be assigned to
596+
/// directly by name, cannot escape their lexical scope, and are always safe
597+
/// to assign. Hence, we only track dirty locals in the write set.
598+
bool must_track_decl_or_dead(
599+
const irep_idt &ident,
600+
const optionalt<cfg_infot> &cfg_info_opt) const;
601+
596602
/// Returns true iff an `ASSIGN lhs := rhs` instruction must be instrumented.
597603
bool must_check_assign(
598604
const goto_programt::const_targett &target,

0 commit comments

Comments
 (0)