You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

599 lines
21 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
open PulseBasicInterface
module L = Logging
type value = AbstractValue.t [@@deriving compare]
type event =
| ArrayWrite of {aw_array: value; aw_index: value}
| Call of {return: value option; arguments: value list; procname: Procname.t}
[@@deriving compare]
let pp_comma_seq f xs = Pp.comma_seq ~print_env:Pp.text_break f xs
let pp_event f = function
| ArrayWrite {aw_array; aw_index} ->
Format.fprintf f "@[ArrayWrite %a[%a]@]" AbstractValue.pp aw_array AbstractValue.pp aw_index
| Call {return; arguments; procname} ->
let procname = Procname.hashable_name procname (* as in [static_match] *) in
Format.fprintf f "@[call@ %a=%s(%a)@]" (Pp.option AbstractValue.pp) return procname
(pp_comma_seq AbstractValue.pp) arguments
type vertex = ToplAutomaton.vindex [@@deriving compare]
type register = ToplAst.register_name [@@deriving compare]
type configuration = {vertex: vertex; memory: (register * value) list} [@@deriving compare]
type substitution = (AbstractValue.t * ValueHistory.t) AbstractValue.Map.t
type 'a substitutor = substitution * 'a -> substitution * 'a
let sub_value (sub, value) =
match AbstractValue.Map.find_opt value sub with
| Some (v, _history) ->
(sub, v)
| None ->
let v = AbstractValue.mk_fresh () in
let sub = AbstractValue.Map.add value (v, []) sub in
(sub, v)
let sub_list : 'a substitutor -> 'a list substitutor =
fun sub_elem (sub, xs) ->
let f (sub, xs) x =
let sub, x = sub_elem (sub, x) in
(sub, x :: xs)
in
let sub, xs = List.fold ~init:(sub, []) ~f xs in
(sub, List.rev xs)
module Constraint : sig
type predicate
type t [@@deriving compare]
type operand = PathCondition.operand
val make : Binop.t -> operand -> operand -> predicate
val true_ : t
val and_predicate : predicate -> t -> t
val and_constr : t -> t -> t
val and_n : t list -> t
val normalize : t -> t
val negate : t list -> t list
(** computes ¬(c1...cm) as d1...dn, where n=|c1|x...x|cm| *)
val eliminate_exists : keep:AbstractValue.Set.t -> t -> t
(** quantifier elimination *)
val size : t -> int
val substitute : t substitutor
val prune_path : t -> PathCondition.t -> PathCondition.t
val pp : Format.formatter -> t -> unit
end = struct
type predicate = Binop.t * PathCondition.operand * PathCondition.operand [@@deriving compare]
type t = predicate list [@@deriving compare]
type operand = PathCondition.operand
let make binop lhs rhs = (binop, lhs, rhs)
let true_ = []
let is_trivially_true (predicate : predicate) =
match predicate with
| Eq, AbstractValueOperand l, AbstractValueOperand r when AbstractValue.equal l r ->
true
| _ ->
false
let and_predicate predicate constr =
if is_trivially_true predicate then constr else predicate :: constr
let and_constr constr_a constr_b = List.rev_append constr_a constr_b
let and_n constraints = List.concat_no_order constraints
let normalize constr = List.dedup_and_sort ~compare:compare_predicate constr
let negate_predicate (predicate : predicate) : predicate =
match predicate with
| Eq, l, r ->
(Ne, l, r)
| Ne, l, r ->
(Eq, l, r)
| Ge, l, r ->
(Lt, r, l)
| Gt, l, r ->
(Le, r, l)
| Le, l, r ->
(Gt, r, l)
| Lt, l, r ->
(Ge, r, l)
| _ ->
L.die InternalError
"PulseTopl.negate_predicate should handle all outputs of ToplUtils.binop_to"
let negate disjunction = IList.product (List.map ~f:(List.map ~f:negate_predicate) disjunction)
let size constr = List.length constr
let substitute_predicate (sub, predicate) =
let avo x : PathCondition.operand = AbstractValueOperand x in
match (predicate : predicate) with
| op, AbstractValueOperand l, AbstractValueOperand r ->
let sub, l = sub_value (sub, l) in
let sub, r = sub_value (sub, r) in
(sub, (op, avo l, avo r))
| op, AbstractValueOperand l, r ->
let sub, l = sub_value (sub, l) in
(sub, (op, avo l, r))
| op, l, AbstractValueOperand r ->
let sub, r = sub_value (sub, r) in
(sub, (op, l, avo r))
| _ ->
(sub, predicate)
let substitute = sub_list substitute_predicate
let prune_path constr path_condition =
let f path_condition (op, l, r) =
let path_condition, _new_eqs =
PathCondition.prune_binop ~negated:false op l r path_condition
in
path_condition
in
List.fold ~init:path_condition ~f constr
let pp_predicate f (op, l, r) =
Format.fprintf f "@[%a%a%a@]" PathCondition.pp_operand l Binop.pp op PathCondition.pp_operand r
let pp = Pp.seq ~sep:"" pp_predicate
let eliminate_exists ~keep constr =
(* TODO(rgrigore): replace the current weak approximation *)
let is_live_operand =
PathCondition.(
function LiteralOperand _ -> true | AbstractValueOperand v -> AbstractValue.Set.mem v keep)
in
let is_live_predicate (_op, l, r) = is_live_operand l && is_live_operand r in
List.filter ~f:is_live_predicate constr
end
type predicate = Binop.t * PathCondition.operand * PathCondition.operand [@@deriving compare]
type step =
{ step_location: Location.t
; step_predecessor: simple_state (** state before this step *)
; step_data: step_data }
and step_data = SmallStep of event | LargeStep of (Procname.t * (* post *) simple_state)
and simple_state =
{ pre: configuration (** at the start of the procedure *)
; post: configuration (** at the current program point *)
; pruned: Constraint.t (** path-condition for the automaton *)
; last_step: step option [@compare.ignore] (** for trace error reporting *) }
[@@deriving compare]
(* TODO: include a hash of the automaton in a summary to avoid caching problems. *)
(* TODO: limit the number of simple_states to some configurable number (default ~5) *)
type state = simple_state list
let pp_mapping f (x, value) = Format.fprintf f "@[%s↦%a@]@," x AbstractValue.pp value
let pp_memory f memory = Format.fprintf f "@[<2>[%a]@]" (pp_comma_seq pp_mapping) memory
let pp_configuration f {vertex; memory} =
Format.fprintf f "@[{ topl-config@;vertex=%d@;memory=%a }@]" vertex pp_memory memory
let pp_simple_state f {pre; post; pruned} =
Format.fprintf f "@[<2>{ topl-simple-state@;pre=%a@;post=%a@;pruned=(%a) }@]" pp_configuration pre
pp_configuration post Constraint.pp pruned
let pp_state f = Format.fprintf f "@[<2>[ %a ]@]" (pp_comma_seq pp_simple_state)
let start () =
let mk_simple_states () =
let a = Topl.automaton () in
let memory =
List.map ~f:(fun r -> (r, AbstractValue.mk_fresh ())) (ToplAutomaton.registers a)
in
let configurations =
let n = ToplAutomaton.vcount a in
let f acc vertex = {vertex; memory} :: acc in
IContainer.forto n ~init:[] ~f
in
List.map
~f:(fun c -> {pre= c; post= c; pruned= Constraint.true_; last_step= None})
configurations
in
if Topl.is_deep_active () then mk_simple_states () else (* Avoids work later *) []
let get env x =
match List.Assoc.find env ~equal:String.equal x with
| Some v ->
v
| None ->
L.die InternalError "TOPL: Cannot find %s. Should be caught by static checks" x
let set = List.Assoc.add ~equal:String.equal
let eval_guard memory tcontext guard : Constraint.t =
let operand_of_value (value : ToplAst.value) : PathCondition.operand =
match value with
| Constant (LiteralInt x) ->
LiteralOperand (IntLit.of_int x)
| Register reg ->
AbstractValueOperand (get memory reg)
| Binding v ->
AbstractValueOperand (get tcontext v)
in
let conjoin_predicate pruned (predicate : ToplAst.predicate) =
match predicate with
| Binop (binop, l, r) ->
let l = operand_of_value l in
let r = operand_of_value r in
let binop = ToplUtils.binop_to binop in
Constraint.and_predicate (Constraint.make binop l r) pruned
| Value v ->
let v = operand_of_value v in
let one = PathCondition.LiteralOperand IntLit.one in
Constraint.and_predicate (Constraint.make Binop.Ne v one) pruned
in
List.fold ~init:Constraint.true_ ~f:conjoin_predicate guard
let apply_action tcontext assignments memory =
let apply_one memory (register, variable) = set memory register (get tcontext variable) in
List.fold ~init:memory ~f:apply_one assignments
type tcontext = (ToplAst.variable_name * AbstractValue.t) list
let pp_tcontext f tcontext =
Format.fprintf f "@[[%a]@]" (pp_comma_seq (Pp.pair ~fst:String.pp ~snd:AbstractValue.pp)) tcontext
let static_match_array_write arr index label : tcontext option =
match label.ToplAst.pattern with
| ArrayWritePattern ->
let v1, v2 =
match label.ToplAst.arguments with
| Some [v1; v2] ->
(v1, v2)
| _ ->
L.die InternalError "Topl: #ArrayWrite should have exactly two arguments"
in
Some [(v1, arr); (v2, index)]
| _ ->
None
let static_match_call return arguments procname label : tcontext option =
let rev_arguments = List.rev arguments in
let procname = Procname.hashable_name procname in
let match_name () : bool =
match label.ToplAst.pattern with
| ProcedureNamePattern pname ->
Str.string_match (Str.regexp pname) procname 0
| _ ->
false
in
let match_args () : tcontext option =
let match_formals formals : tcontext option =
let bind ~init rev_formals =
let f tcontext variable value = (variable, value) :: tcontext in
match List.fold2 ~init ~f rev_formals rev_arguments with
| Ok c ->
Some c
| Unequal_lengths ->
None
in
match (List.rev formals, return) with
| [], Some _ ->
None
| rev_formals, None ->
bind ~init:[] rev_formals
| r :: rev_formals, Some v ->
bind ~init:[(r, v)] rev_formals
in
Option.value_map ~default:(Some []) ~f:match_formals label.ToplAst.arguments
in
if match_name () then match_args () else None
(** Returns a list of transitions whose pattern matches (e.g., event type matches). Each match
produces a tcontext (transition context), which matches transition-local variables to abstract
values. *)
let static_match event : (ToplAutomaton.transition * tcontext) list =
let match_one transition =
let f label =
match event with
| ArrayWrite {aw_array; aw_index} ->
static_match_array_write aw_array aw_index label
| Call {return; arguments; procname} ->
static_match_call return arguments procname label
in
let tcontext_opt = Option.value_map ~default:(Some []) ~f transition.ToplAutomaton.label in
L.d_printfln "@[<2>PulseTopl.static_match:@;transition %a@;event %a@;result %a@]"
ToplAutomaton.pp_transition transition pp_event event (Pp.option pp_tcontext) tcontext_opt ;
Option.map ~f:(fun tcontext -> (transition, tcontext)) tcontext_opt
in
ToplAutomaton.tfilter_map (Topl.automaton ()) ~f:match_one
let is_unsat_cheap path_condition pruned =
PathCondition.is_unsat_cheap (Constraint.prune_path pruned path_condition)
let is_unsat_expensive path_condition pruned =
let _path_condition, unsat, _new_eqs =
PathCondition.is_unsat_expensive (Constraint.prune_path pruned path_condition)
in
unsat
let drop_infeasible ?(expensive = false) path_condition state =
let is_unsat = if expensive then is_unsat_expensive else is_unsat_cheap in
let f {pruned} = not (is_unsat path_condition pruned) in
List.filter ~f state
let normalize_memory memory = List.sort ~compare:[%compare: register * value] memory
let normalize_configuration {vertex; memory} = {vertex; memory= normalize_memory memory}
let normalize_simple_state {pre; post; pruned; last_step} =
{ pre= normalize_configuration pre
; post= normalize_configuration post
; pruned= Constraint.normalize pruned
; last_step }
let normalize_state state = List.map ~f:normalize_simple_state state
let apply_conjuncts_limit state =
let f simple_state = Constraint.size simple_state.pruned <= Config.topl_max_conjuncts in
IList.filter_changed ~f state
let apply_disjuncts_limit state =
if List.length state <= Config.topl_max_disjuncts then state
else
let new_len = (Config.topl_max_disjuncts / 2) + 1 in
let add_score simple_state = (Constraint.size simple_state.pruned, simple_state) in
let compare_score (score1, _simple_state1) (score2, _simple_state2) =
Int.compare score1 score2
in
let strip_score (_score, simple_state) = simple_state in
state |> List.map ~f:add_score |> List.sort ~compare:compare_score |> Fn.flip List.take new_len
|> List.map ~f:strip_score
let apply_limits state = state |> apply_conjuncts_limit |> apply_disjuncts_limit
let small_step loc path_condition event simple_states =
let simple_states = apply_limits simple_states in
let tmatches = static_match event in
let evolve_transition (old : simple_state) (transition, tcontext) : state =
let mk ?(memory = old.post.memory) ?(pruned = Constraint.true_) significant =
let last_step =
if significant then
Some {step_location= loc; step_predecessor= old; step_data= SmallStep event}
else old.last_step
in
(* NOTE: old pruned is discarded, because evolve_simple_state needs to see only new prunes
to determine skip transitions. It will then add back old prunes. *)
let post = {vertex= transition.ToplAutomaton.target; memory} in
{old with post; pruned; last_step}
in
match transition.ToplAutomaton.label with
| None ->
(* "any" transition *)
let is_loop = Int.equal transition.ToplAutomaton.source transition.ToplAutomaton.target in
[mk (not is_loop)]
| Some label ->
let memory = old.post.memory in
let pruned = eval_guard memory tcontext label.ToplAst.condition in
let memory = apply_action tcontext label.ToplAst.action memory in
[mk ~memory ~pruned true]
in
let evolve_simple_state old =
let path_condition = Constraint.prune_path old.pruned path_condition in
let tmatches =
List.filter ~f:(fun (t, _) -> Int.equal old.post.vertex t.ToplAutomaton.source) tmatches
in
let nonskip =
drop_infeasible path_condition (List.concat_map ~f:(evolve_transition old) tmatches)
in
let skip =
let nonskip_disjunction = List.map ~f:(fun {pruned} -> pruned) nonskip in
let skip_disjunction = Constraint.negate nonskip_disjunction in
let f pruned = {old with pruned} (* keeps last_step from old *) in
drop_infeasible path_condition (List.map ~f skip_disjunction)
in
let add_old_pruned s = {s with pruned= Constraint.and_constr s.pruned old.pruned} in
List.map ~f:add_old_pruned (List.rev_append nonskip skip)
in
let result = List.concat_map ~f:evolve_simple_state simple_states in
L.d_printfln "@[<2>PulseTopl.small_step:@;%a@ -> %a@]" pp_state simple_states pp_state result ;
result
let of_unequal (or_unequal : 'a List.Or_unequal_lengths.t) =
match or_unequal with
| Ok x ->
x
| Unequal_lengths ->
L.die InternalError "PulseTopl expected lists to be of equal lengths"
let sub_configuration (sub, {vertex; memory}) =
let keys, values = List.unzip memory in
let sub, values = sub_list sub_value (sub, values) in
let memory = of_unequal (List.zip keys values) in
(sub, {vertex; memory})
(* Does not substitute in [last_step]: not usually needed, and takes much time. *)
let sub_simple_state (sub, {pre; post; pruned; last_step}) =
let sub, pre = sub_configuration (sub, pre) in
let sub, post = sub_configuration (sub, post) in
let sub, pruned = Constraint.substitute (sub, pruned) in
(sub, {pre; post; pruned; last_step})
let large_step ~call_location ~callee_proc_name ~substitution ~condition ~callee_prepost state =
let state = apply_limits state in
let seq ((p : simple_state), (q : simple_state)) =
if not (Int.equal p.post.vertex q.pre.vertex) then None
else
let substitution, new_eqs =
(* Update the substitution, matching formals with actuals. We work a bit to avoid introducing
equalities, because a growing [pruned] leads to quadratic behaviour. *)
let mk_eq val1 val2 =
let op x = PathCondition.AbstractValueOperand x in
Constraint.make Binop.Eq (op val1) (op val2)
in
let f (sub, eqs) (reg1, val1) (reg2, val2) =
if not (String.equal reg1 reg2) then
L.die InternalError
"PulseTopl: normalized memories are expected to have the same domain"
else
match AbstractValue.Map.find_opt val2 sub with
| Some (old_val1, _history) ->
if AbstractValue.equal old_val1 val1 then (sub, eqs)
else (sub, Constraint.and_predicate (mk_eq old_val1 val1) eqs)
| None ->
(AbstractValue.Map.add val2 (val1, []) sub, eqs)
in
of_unequal (List.fold2 p.post.memory q.pre.memory ~init:(substitution, Constraint.true_) ~f)
in
let _substitution, q = sub_simple_state (substitution, q) in
let pruned = Constraint.and_n [new_eqs; q.pruned; p.pruned] in
let last_step =
Some
{ step_location= call_location
; step_predecessor= p
; step_data= LargeStep (callee_proc_name, q) }
in
Some {pre= p.pre; post= q.post; pruned; last_step}
in
(* TODO(rgrigore): may be worth optimizing the cartesian_product *)
let state = normalize_state state in
let callee_prepost = normalize_state callee_prepost in
let new_state = List.filter_map ~f:seq (List.cartesian_product state callee_prepost) in
let result = drop_infeasible condition new_state in
L.d_printfln "@[<2>PulseTopl.large_step:@;callee_prepost=%a@;%a@ -> %a@]" pp_state callee_prepost
pp_state state pp_state result ;
result
let filter_for_summary path_condition state = drop_infeasible ~expensive:true path_condition state
let simplify ~keep state =
let simplify_simple_state {pre; post; pruned; last_step} =
(* NOTE(rgrigore): registers could be considered live for the program path_condition as well.
That should improve precision, but I'm wary of altering what the Pulse program state is just
because Topl is enabled. *)
let collect memory keep =
List.fold ~init:keep ~f:(fun keep (_reg, value) -> AbstractValue.Set.add value keep) memory
in
let keep = keep |> collect pre.memory |> collect post.memory in
let pruned = Constraint.eliminate_exists ~keep pruned in
{pre; post; pruned; last_step}
in
let state = List.map ~f:simplify_simple_state state in
List.dedup_and_sort ~compare:compare_simple_state state
let description_of_step_data step_data =
( match step_data with
| SmallStep (Call {procname}) | LargeStep (procname, _) ->
Format.fprintf Format.str_formatter "@[call to %a@]" Procname.pp procname
| SmallStep (ArrayWrite _) ->
Format.fprintf Format.str_formatter "@[write to array@]" ) ;
Format.flush_str_formatter ()
let report_errors proc_desc err_log state =
let a = Topl.automaton () in
let rec make_trace nesting trace q =
match q.last_step with
| None ->
trace
| Some {step_location; step_predecessor; step_data} ->
let description = description_of_step_data step_data in
let trace =
let trace_element = Errlog.make_trace_element nesting step_location description [] in
match step_data with
| SmallStep _ ->
trace_element :: trace
| LargeStep (_, {last_step= None}) ->
trace (* skip trivial large steps (i.e., those with no steps) *)
| LargeStep (_, qq) ->
trace_element :: make_trace (nesting + 1) trace qq
in
make_trace nesting trace step_predecessor
in
let rec first_error_ss q =
match q.last_step with
| Some {step_predecessor} ->
if not (ToplAutomaton.is_error a step_predecessor.post.vertex) then q
else first_error_ss step_predecessor
| None ->
L.die InternalError "PulseTopl.report_errors inv broken"
in
let is_nested_large_step q =
match q.last_step with
| Some {step_data= LargeStep (_, prepost)}
when ToplAutomaton.is_start a prepost.pre.vertex
&& ToplAutomaton.is_error a prepost.post.vertex ->
true
| _ ->
false
in
let report_simple_state q =
if ToplAutomaton.is_start a q.pre.vertex && ToplAutomaton.is_error a q.post.vertex then
let q = first_error_ss q in
(* Only report at the innermost level where error appears. *)
if not (is_nested_large_step q) then
let loc = Procdesc.get_loc proc_desc in
let ltr = make_trace 0 [] q in
let message = Format.asprintf "%a" ToplAutomaton.pp_message_of_state (a, q.post.vertex) in
Reporting.log_issue proc_desc err_log ~loc ~ltr ToplOnPulse IssueType.topl_pulse_error
message
in
List.iter ~f:report_simple_state state