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.

341 lines
14 KiB

(*
* 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
module F = Format
module L = Logging
open IResult.Let_syntax
open PulseBasicInterface
open PulseDomainInterface
let exec_list_of_list_result = function
| Ok posts ->
posts
| Error PulseReport.InfeasiblePath ->
[]
| Error (PulseReport.FeasiblePath post) ->
[post]
let report_on_error {InterproceduralAnalysis.proc_desc; err_log} result =
PulseReport.report_error proc_desc err_log result
>>| (fun post -> [ExecutionDomain.ContinueProgram post])
|> exec_list_of_list_result
let report_on_error_list {InterproceduralAnalysis.proc_desc; err_log} result =
PulseReport.report_error proc_desc err_log result |> exec_list_of_list_result
let proc_name_of_call call_exp =
match (call_exp : Exp.t) with
| Const (Cfun proc_name) | Closure {name= proc_name} ->
Some proc_name
| _ ->
None
module PulseTransferFunctions = struct
module CFG = ProcCfg.Normal
module Domain = ExecutionDomain
type analysis_data = PulseSummary.t InterproceduralAnalysis.t
let get_pvar_formals pname =
AnalysisCallbacks.proc_resolve_attributes pname |> Option.map ~f:ProcAttributes.get_pvar_formals
let interprocedural_call {InterproceduralAnalysis.analyze_dependency; proc_desc; err_log} ret
call_exp actuals call_loc astate =
match proc_name_of_call call_exp with
| Some callee_pname when not Config.pulse_intraprocedural_only ->
let formals_opt = get_pvar_formals callee_pname in
let callee_data = analyze_dependency callee_pname in
PulseOperations.call ~caller_proc_desc:proc_desc err_log ~callee_data call_loc callee_pname
~ret ~actuals ~formals_opt astate
| _ ->
L.d_printfln "Skipping indirect call %a@\n" Exp.pp call_exp ;
Ok
[ Domain.ContinueProgram
(PulseOperations.unknown_call call_loc (SkippedUnknownCall call_exp) ~ret ~actuals
~formals_opt:None astate) ]
(** has an object just gone out of scope? *)
let get_out_of_scope_object call_exp actuals (flags : CallFlags.t) =
(* injected destructors are precisely inserted where an object goes out of scope *)
if flags.cf_injected_destructor then
match (proc_name_of_call call_exp, actuals) with
| Some (Procname.ObjC_Cpp pname), [(Exp.Lvar pvar, typ)]
when Pvar.is_local pvar && not (Procname.ObjC_Cpp.is_inner_destructor pname) ->
(* ignore inner destructors, only trigger out of scope on the final destructor call *)
Some (pvar, typ)
| _ ->
None
else None
(** [out_of_scope_access_expr] has just gone out of scope and in now invalid *)
let exec_object_out_of_scope call_loc (pvar, typ) exec_state =
match (exec_state : ExecutionDomain.t) with
| ContinueProgram astate ->
let gone_out_of_scope = Invalidation.GoneOutOfScope (pvar, typ) in
let* astate, out_of_scope_base = PulseOperations.eval call_loc (Exp.Lvar pvar) astate in
(* invalidate [&x] *)
PulseOperations.invalidate call_loc gone_out_of_scope out_of_scope_base astate
>>| ExecutionDomain.continue
| AbortProgram _ | ExitProgram _ | LatentAbortProgram _ ->
Ok exec_state
let topl_small_step procname arguments (return, _typ) exec_state_res =
let arguments =
List.map arguments ~f:(fun {ProcnameDispatcher.Call.FuncArg.arg_payload} -> fst arg_payload)
in
let return = Var.of_id return in
let do_astate astate =
let return = Option.map ~f:fst (Stack.find_opt return astate) in
let topl_event = PulseTopl.Call {return; arguments; procname} in
AbductiveDomain.Topl.small_step topl_event astate
in
let do_one_exec_state (exec_state : Domain.t) : Domain.t =
match exec_state with
| ContinueProgram astate ->
ContinueProgram (do_astate astate)
| AbortProgram _ | LatentAbortProgram _ | ExitProgram _ ->
exec_state
in
Result.map ~f:(List.map ~f:do_one_exec_state) exec_state_res
let dispatch_call ({InterproceduralAnalysis.tenv} as analysis_data) ret call_exp actuals call_loc
flags astate =
(* evaluate all actuals *)
let* astate, rev_func_args =
List.fold_result actuals ~init:(astate, [])
~f:(fun (astate, rev_func_args) (actual_exp, actual_typ) ->
let+ astate, actual_evaled = PulseOperations.eval call_loc actual_exp astate in
( astate
, ProcnameDispatcher.Call.FuncArg.
{exp= actual_exp; arg_payload= actual_evaled; typ= actual_typ}
:: rev_func_args ) )
in
let func_args = List.rev rev_func_args in
let callee_pname = proc_name_of_call call_exp in
let model =
match callee_pname with
| Some callee_pname ->
PulseModels.dispatch tenv callee_pname func_args
|> Option.map ~f:(fun model -> (model, callee_pname))
| None ->
(* function pointer, etc.: skip for now *)
None
in
(* do interprocedural call then destroy objects going out of scope *)
let exec_state_res =
match model with
| Some (model, callee_procname) ->
L.d_printfln "Found model for call@\n" ;
model analysis_data ~callee_procname call_loc ~ret astate
| None ->
PerfEvent.(log (fun logger -> log_begin_event logger ~name:"pulse interproc call" ())) ;
let only_actuals_evaled =
List.map func_args ~f:(fun ProcnameDispatcher.Call.FuncArg.{arg_payload; typ} ->
(arg_payload, typ) )
in
let r =
interprocedural_call analysis_data ret call_exp only_actuals_evaled call_loc astate
in
PerfEvent.(log (fun logger -> log_end_event logger ())) ;
r
in
let exec_state_res =
if Topl.is_deep_active () then
match callee_pname with
| Some callee_pname ->
topl_small_step callee_pname func_args ret exec_state_res
| None ->
(* skip, as above for non-topl *) exec_state_res
else exec_state_res
in
match get_out_of_scope_object call_exp actuals flags with
| Some pvar_typ ->
L.d_printfln "%a is going out of scope" Pvar.pp_value (fst pvar_typ) ;
let* exec_states = exec_state_res in
List.map exec_states ~f:(fun exec_state ->
exec_object_out_of_scope call_loc pvar_typ exec_state )
|> Result.all
| None ->
exec_state_res
(* [get_dealloc_from_dynamic_types vars_types loc] returns a dealloc procname and vars and
type needed to execute a call to dealloc for the given variables for which the dynamic type
is an Objective-C class. *)
let get_dealloc_from_dynamic_types dynamic_types_unreachable =
let get_dealloc (var, name) =
let cls_typ = Typ.mk (Typ.Tstruct name) in
match Var.get_ident var with
| Some id when Typ.is_objc_class cls_typ ->
let ret_id = Ident.create_fresh Ident.knormal in
let dealloc = Procname.make_objc_dealloc name in
let typ = Typ.mk_ptr cls_typ in
Some (ret_id, id, typ, dealloc)
| _ ->
None
in
List.filter_map ~f:get_dealloc dynamic_types_unreachable
(* In the case of variables that point to Objective-C classes for which we have a dynamic type, we
add and execute calls to dealloc. The main advantage of adding this calls
is that some memory could be freed in dealloc, and we would be reporting a leak on it if we
didn't call it. *)
let execute_injected_dealloc_calls analysis_data vars astate location =
let used_ids = Stack.keys astate |> List.filter_map ~f:(fun var -> Var.get_ident var) in
Ident.update_name_generator used_ids ;
let call_dealloc (astate_list : Domain.t list) (ret_id, id, typ, dealloc) =
let ret = (ret_id, StdTyp.void) in
let call_flags = CallFlags.default in
let call_exp = Exp.Const (Cfun dealloc) in
let actuals = [(Exp.Var id, typ)] in
let call_instr = Sil.Call (ret, call_exp, actuals, location, call_flags) in
L.d_printfln ~color:Pp.Orange "@\nExecuting injected instr:%a@\n@."
(Sil.pp_instr Pp.text ~print_types:true)
call_instr ;
List.fold
~f:(fun astates (astate : Domain.t) ->
let astate =
match astate with
| AbortProgram _ | ExitProgram _ | LatentAbortProgram _ ->
[astate]
| ContinueProgram astate ->
dispatch_call analysis_data ret call_exp actuals location call_flags astate
|> report_on_error_list analysis_data
in
List.rev_append astate astates )
~init:[] astate_list
in
let dynamic_types_unreachable =
PulseOperations.get_dynamic_type_unreachable_values vars astate
in
let dealloc_data = get_dealloc_from_dynamic_types dynamic_types_unreachable in
let ret_vars = List.map ~f:(fun (ret_id, _, _, _) -> Var.of_id ret_id) dealloc_data in
L.d_printfln ~color:Pp.Orange
"Executing injected call to dealloc for vars (%a) that are exiting the scope@."
(Pp.seq ~sep:"," Var.pp) vars ;
let astates = List.fold ~f:call_dealloc dealloc_data ~init:[Domain.ContinueProgram astate] in
(astates, ret_vars)
let exec_instr (astate : Domain.t) ({InterproceduralAnalysis.proc_desc} as analysis_data)
_cfg_node (instr : Sil.instr) : Domain.t list =
match astate with
| AbortProgram _ | LatentAbortProgram _ ->
(* We can also continue the analysis with the error state here
but there might be a risk we would get nonsense. *)
[astate]
| ExitProgram _ ->
(* program already exited, simply propagate the exited state upwards *)
[astate]
| ContinueProgram astate -> (
match instr with
| Load {id= lhs_id; e= rhs_exp; loc} ->
(* [lhs_id := *rhs_exp] *)
let result =
let+ astate, rhs_addr_hist = PulseOperations.eval_deref loc rhs_exp astate in
PulseOperations.write_id lhs_id rhs_addr_hist astate
in
report_on_error analysis_data result
| Store {e1= lhs_exp; e2= rhs_exp; loc} ->
(* [*lhs_exp := rhs_exp] *)
let event = ValueHistory.Assignment loc in
let result =
let* astate, (rhs_addr, rhs_history) = PulseOperations.eval loc rhs_exp astate in
let* astate, lhs_addr_hist = PulseOperations.eval loc lhs_exp astate in
let* astate =
PulseOperations.write_deref loc ~ref:lhs_addr_hist
~obj:(rhs_addr, event :: rhs_history)
astate
in
match lhs_exp with
| Lvar pvar when Pvar.is_return pvar ->
PulseOperations.check_address_escape loc proc_desc rhs_addr rhs_history astate
| _ ->
Ok astate
in
report_on_error analysis_data result
| Prune (condition, loc, _is_then_branch, _if_kind) ->
(let+ astate = PulseOperations.prune loc ~condition astate in
if PulseArithmetic.is_unsat_cheap astate then
(* [condition] is known to be unsatisfiable: prune path *)
[]
else
(* [condition] is true or unknown value: go into the branch *)
[Domain.ContinueProgram astate])
|> report_on_error_list analysis_data
| Call (ret, call_exp, actuals, loc, call_flags) ->
dispatch_call analysis_data ret call_exp actuals loc call_flags astate
|> report_on_error_list analysis_data
| Metadata (ExitScope (vars, location)) ->
let remove_vars vars astates =
List.fold
~f:(fun astates (astate : Domain.t) ->
match astate with
| AbortProgram _ | ExitProgram _ | LatentAbortProgram _ ->
[astate]
| ContinueProgram astate ->
let astate =
PulseOperations.remove_vars vars location astate
|> report_on_error analysis_data
in
List.rev_append astate astates )
~init:[] astates
in
if Procname.is_java (Procdesc.get_proc_name proc_desc) then
remove_vars vars [Domain.ContinueProgram astate]
else
(* Here we add and execute calls to dealloc for Objective-C objects
before removing the variables *)
let astates, ret_vars =
execute_injected_dealloc_calls analysis_data vars astate location
in
(* OPTIM: avoid re-allocating [vars] when [ret_vars] is empty
(in particular if no ObjC objects are involved), but otherwise
assume [ret_vars] is potentially larger than [vars] and so
append [vars] to [ret_vars]. *)
let vars_to_remove =
if List.is_empty ret_vars then vars else List.rev_append vars ret_vars
in
remove_vars vars_to_remove astates
| Metadata (VariableLifetimeBegins (pvar, _, location)) when not (Pvar.is_global pvar) ->
[PulseOperations.realloc_pvar pvar location astate |> Domain.continue]
| Metadata (Abstract _ | VariableLifetimeBegins _ | Nullify _ | Skip) ->
[Domain.ContinueProgram astate] )
let pp_session_name _node fmt = F.pp_print_string fmt "Pulse"
end
module DisjunctiveAnalyzer =
AbstractInterpreter.MakeDisjunctive
(PulseTransferFunctions)
(struct
let join_policy = `UnderApproximateAfter Config.pulse_max_disjuncts
let widen_policy = `UnderApproximateAfterNumIterations Config.pulse_widen_threshold
end)
let checker ({InterproceduralAnalysis.proc_desc} as analysis_data) =
AbstractValue.State.reset () ;
let initial = [ExecutionDomain.mk_initial proc_desc] in
match DisjunctiveAnalyzer.compute_post analysis_data ~initial proc_desc with
| Some posts ->
Some (PulseSummary.of_posts proc_desc posts)
| None ->
None