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.

351 lines
11 KiB

(*
* Copyright (c) 2016 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
open! Utils
module L = Logging
module F = Format
open Dataflow
(** Check the sequence of calls to tracing APIs in a method (intraprocedural). *)
let verbose = false
let widening_threshold = 10
(** Tracing methods of the form (package name, class name, begin name, end name). *)
let tracing_methods =
let for_testing =
[
"", "TraceCallSequence", "begin", "end";
"", "TraceCallSequence", "beginWrapper", "endWrapper";
] in
for_testing @ FbTraceCalls.tracing_methods
(** Boolean variables to be tracked. *)
let boolean_variables =
[
"shouldTrace";
]
(** Report a warning in the spec file of the procedure. *)
let report_warning tenv description pn pd loc =
if verbose then L.stderr "ERROR: %s@." description;
Checkers.ST.report_error tenv pn pd "CHECKERS_TRACE_CALLS_SEQUENCE" loc description
(** Tracing APIs. *)
module APIs = struct
let method_match pn pkgname cname mname =
match pn with
| Procname.Java pn_java ->
Procname.java_get_method pn_java = mname
&&
(match pkgname with
| "" ->
Procname.java_get_simple_class_name pn_java = cname
| _ ->
Procname.java_get_class_name pn_java = pkgname ^ "." ^ cname)
| _ ->
false
let is_begin pn =
let filter (pkgname, cname, begin_name, _) = method_match pn pkgname cname begin_name in
IList.exists filter tracing_methods
let is_end pn =
let filter (pkgname, cname, _, end_name) = method_match pn pkgname cname end_name in
IList.exists filter tracing_methods
let is_begin_or_end pn =
is_begin pn || is_end pn
end
(** Environment for boolean variables. *)
module Env = struct
type t = bool StringMap.t
let empty = StringMap.empty
let compare = StringMap.compare bool_compare
let add = StringMap.add
let remove = StringMap.remove
let get map name =
try Some (StringMap.find name map)
with Not_found -> None
let pp fmt map =
let pp_elem fmt (s, b) = F.fprintf fmt "%s:%b" s b in
let l = StringMap.bindings map in
if l <> [] then F.fprintf fmt "%a" (pp_seq pp_elem) l
end
(** Element for the set domain: an integer (for pending traces), and an environment. *)
module Elem = struct
type t = int * Env.t
let compare (i1, env1) (i2, env2) =
let n = int_compare i1 i2 in
if n <> 0 then n else Env.compare env1 env2
let pp fmt (i, env) = F.fprintf fmt "(%d %a)" i Env.pp env
let zero = (0, Env.empty)
let is_consistent (i, _) = i >= 0
let get_int (i, _) = i
let get_env (_, env) = env
let set_env (i, _) env' = (i, env')
let incr (i, env) = (i + 1, env)
let decr (i, env) = (i - 1, env)
end
module ElemSet = Set.Make(Elem)
module State = struct
(** Each number represents the difference between begin() and end() on some path. *)
type t = ElemSet.t
let to_string n =
let pp_intset fmt s =
ElemSet.iter (fun d -> F.fprintf fmt "%a " Elem.pp d) s in
let pp fmt () =
Format.fprintf fmt "{%a}" pp_intset n in
pp_to_string pp ()
(** Initial balanced state with empty environment. *)
let balanced = ElemSet.singleton Elem.zero
(** State is balanced. *)
let is_balanced s =
ElemSet.for_all (fun elem -> Elem.get_int elem = 0) s
let equal = ElemSet.equal
let has_zero s = ElemSet.exists (fun elem -> Elem.get_int elem = 0) s
(** Map a function to the elements of the set, and filter out inconsistencies. *)
let map2 (f : Elem.t -> Elem.t list) (s : t) : t =
let l = ElemSet.elements s in
let l' = IList.filter Elem.is_consistent (IList.flatten (IList.map f l)) in
IList.fold_right ElemSet.add l' ElemSet.empty
let map (f : Elem.t -> Elem.t) s =
map2 (fun elem -> [f elem]) s
(** Increment the number of begin/start. *)
let incr s =
map Elem.incr s
(** Decrement the number of begin/start. *)
let decr s =
map Elem.decr s
(** Max value of the set. *)
let max s =
ElemSet.max_elt s
(** Prune the value of name w.r.t. boolean b. *)
let prune s name b =
let f elem =
let env = Elem.get_env elem in
match Env.get env name with
| None ->
let env' = Env.add name b env in
let elem' = Elem.set_env elem env' in
[elem']
| Some b' ->
if bool_equal b b' then [elem]
else [] in
map2 f s
(** Set name to the given bool, or nondeterministic if None. *)
let set_bool s name b_opt =
let f elem =
let env = Elem.get_env elem in
let env' = match b_opt with
| None -> Env.remove name env
| Some b -> Env.add name b env in
Elem.set_env elem env' in
map f s
(** Stop growing sets if they go beyond a threshold. *)
let widening s_before s_after =
if ElemSet.cardinal s_after <= widening_threshold
then s_after
else s_before
(** Join two states and perform widening if necessary. *)
let join s1 s2 =
let s' = ElemSet.union s1 s2 in
let s'' = widening s1 s' in
if verbose then L.stderr " join %s %s --> %s@." (to_string s1) (to_string s2) (to_string s'');
s''
end
(** Abstract domain for automata whose state transitions are triggered by calls. *)
module Automaton = struct
(** Transfer function for a procedure call. *)
let do_call tenv caller_pn caller_pd callee_pn (s : State.t) loc : State.t =
let method_name () = match callee_pn with
| Procname.Java pname_java ->
Procname.java_get_method pname_java
| _ ->
Procname.to_simplified_string callee_pn in
if APIs.is_begin callee_pn then
begin
if verbose then L.stderr " calling %s@." (method_name ());
State.incr s
end
else if APIs.is_end callee_pn then
begin
if verbose then L.stderr " calling %s@." (method_name ());
if State.has_zero s then report_warning tenv "too many end/stop" caller_pn caller_pd loc;
State.decr s
end
else s
(** Transfer function for an instruction. *)
let do_instr tenv pn pd (instr : Sil.instr) (state : State.t) : State.t =
match instr with
| Sil.Call (_, Exp.Const (Const.Cfun callee_pn), _, loc, _) ->
do_call tenv pn pd callee_pn state loc
| _ -> state
(** Check if the final state contains any numbers other than zero (balanced). *)
let check_final_state tenv pn pd exit_node (s : State.t) : unit =
if verbose then L.stderr "@.Final: %s@." (State.to_string s);
if not (State.is_balanced s) then
begin
let description = Printf.sprintf "%d missing end/stop" (Elem.get_int (State.max s)) in
let loc = Procdesc.Node.get_loc exit_node in
report_warning tenv description pn pd loc
end
end
(** Abstract domain to track the value of specific boolean variables. *)
module BooleanVars = struct
(** Check if the expression exp is one of the listed boolean variables. *)
let exp_boolean_var exp = match exp with
| Exp.Lvar pvar when Pvar.is_local pvar ->
let name = Mangled.to_string (Pvar.get_name pvar) in
if IList.mem string_equal name boolean_variables
then Some name
else None
| _ -> None
(** Transfer function for an instruction. *)
let do_instr _ _ idenv (instr : Sil.instr) (state : State.t) : State.t =
(* Normalize a boolean condition. *)
let normalize_condition cond_e =
match cond_e with
| Exp.UnOp (Unop.LNot, Exp.BinOp (Binop.Eq, e1, e2), _) ->
Exp.BinOp (Binop.Ne, e1, e2)
| Exp.UnOp (Unop.LNot, Exp.BinOp (Binop.Ne, e1, e2), _) ->
Exp.BinOp (Binop.Eq, e1, e2)
| _ -> cond_e in
(* Normalize an instruction. *)
let normalize_instr = function
| Sil.Prune (cond_e, loc, tb, ik) ->
let cond_e' = normalize_condition cond_e in
Sil.Prune (cond_e', loc, tb, ik)
| instr -> instr in
match normalize_instr instr with
| Sil.Prune (Exp.BinOp (Binop.Eq, _cond_e, Exp.Const (Const.Cint i)), _, _, _)
when IntLit.iszero i ->
let cond_e = Idenv.expand_expr idenv _cond_e in
let state' = match exp_boolean_var cond_e with
| Some name ->
if verbose then L.stderr " prune false on %s@." name;
State.prune state name false
| None -> state in
state'
| Sil.Prune (Exp.BinOp (Binop.Ne, _cond_e, Exp.Const (Const.Cint i)), _, _, _)
when IntLit.iszero i ->
let cond_e = Idenv.expand_expr idenv _cond_e in
let state' = match exp_boolean_var cond_e with
| Some name ->
if verbose then L.stderr " prune true on %s@." name;
State.prune state name true
| None -> state in
state'
| Sil.Store (_e1, _, e2, _) ->
let e1 = Idenv.expand_expr idenv _e1 in
let state' = match exp_boolean_var e1 with
| Some name ->
let b_opt = match e2 with
| Exp.Const (Const.Cint i) -> Some (not (IntLit.iszero i))
| _ -> None in
if verbose then
begin
let b_str = match b_opt with
| None -> "nondet"
| Some b -> string_of_bool b in
L.stderr " setting %s to %s@." name b_str
end;
State.set_bool state name b_opt
| None -> state in
state'
| _ -> state
let check_final_state _ _ _ _ =
()
end
(** State transformation for a cfg node. *)
let do_node tenv pn pd idenv _ node (s : State.t) : (State.t list) * (State.t list) =
if verbose then L.stderr "N:%d S:%s@." (Procdesc.Node.get_id node :> int) (State.to_string s);
let curr_state = ref s in
let do_instr instr : unit =
let state1 = Automaton.do_instr tenv pn pd instr !curr_state in
let state2 = BooleanVars.do_instr pn pd idenv instr state1 in
curr_state := state2 in
IList.iter do_instr (Procdesc.Node.get_instrs node);
[!curr_state], [!curr_state]
(** Check the final state at the end of the analysis. *)
let check_final_state tenv proc_name proc_desc exit_node final_s =
Automaton.check_final_state tenv proc_name proc_desc exit_node final_s;
BooleanVars.check_final_state proc_name proc_desc exit_node final_s
(** Check that the trace calls are balanced.
This is done by using the most general control flow including exceptions.
The begin() and end() function are assumed not to throw exceptions. *)
let callback_check_trace_call_sequence { Callbacks.proc_desc; proc_name; idenv; tenv } : unit =
let module DFTrace = MakeDF(struct
type t = State.t
let equal = State.equal
let join = State.join
let do_node = do_node tenv proc_name proc_desc idenv
let proc_throws pn =
if APIs.is_begin_or_end pn
then DoesNotThrow (* assume the tracing function do not throw *)
else DontKnow
end) in
let do_check () =
begin
if verbose then L.stderr "@.--@.PROC: %a@." Procname.pp proc_name;
let transitions = DFTrace.run tenv proc_desc State.balanced in
let exit_node = Procdesc.get_exit_node proc_desc in
match transitions exit_node with
| DFTrace.Transition (final_s, _, _) ->
check_final_state tenv proc_name proc_desc exit_node final_s
| DFTrace.Dead_state -> ()
end in
if not (APIs.is_begin_or_end proc_name) then do_check ()