[pulse] Distinguish exit state at top level

Summary:
This diff lifts the `PulseAbductiveDomain.t` in `PulseExecutionState` by tracking whether the program continues the analysis normally or exits unusually (e.g. by calling `exit` or `throw`):

```
type exec_state =
  | ContinueProgram of PulseAbductiveDomain.t  (** represents the state at the program point *)
  | ExitProgram of PulseAbductiveDomain.t
      (** represents the state originating at exit/divergence. *)

```

Now, Pulse's actual domain is tracked by `PulseExecutionState` and as soon as we try to analyze an instruction at `ExitProgram`, we simply return its state.

The aim is to recover the state at the time of the exit, rather than simply ignoring them (i.e. returning  empty disjuncts). This allows us to get rid of some FNs that we were not able to detect before. Moreover, it also allows the impurity analysis to be more precise since we will know how the state changed up to exit.

TODO:
- Impurity analysis needs to be improved to consider functions that simply exit as impure.
- The next goal is to handle error state similarly so that when pulse finds an error, we recover the state at the error location (and potentially continue to analyze?).

Disclaimer: currently, we handle throw statements like exit (as was the case before). However, this is not correct. Ideally, control flow from throw nodes follows catch nodes rather than exiting the program entirely.

Reviewed By: jvillard

Differential Revision: D20791747

fbshipit-source-id: df9e5445a
master
Ezgi Çiçek 5 years ago committed by Facebook GitHub Bot
parent f0afa18cbf
commit 5a2b285fff

@ -138,9 +138,12 @@ let is_modeled_pure tenv pname =
(** Given Pulse summary, extract impurity info, i.e. parameters and global variables that are
modified by the function and skipped functions. *)
let extract_impurity tenv pdesc pre_post : ImpurityDomain.t =
let pre_heap = (AbductiveDomain.PrePost.get_pre pre_post).BaseDomain.heap in
let post = AbductiveDomain.PrePost.get_post pre_post in
let extract_impurity tenv pdesc (exec_state : PulseExecutionState.t) : ImpurityDomain.t =
match exec_state with
| ExitProgram astate | ContinueProgram astate ->
(* TODO: consider impure even though the program only exits with pre=post *)
let pre_heap = (PulseAbductiveDomain.get_pre astate).BaseDomain.heap in
let post = PulseAbductiveDomain.get_post astate in
let post_stack = post.BaseDomain.stack in
let pname = Procdesc.get_proc_name pdesc in
let modified_params =
@ -148,7 +151,7 @@ let extract_impurity tenv pdesc pre_post : ImpurityDomain.t =
in
let modified_globals = get_modified_globals pre_heap post post_stack in
let skipped_calls =
AbductiveDomain.PrePost.get_skipped_calls pre_post
PulseAbductiveDomain.get_skipped_calls astate
|> PulseAbductiveDomain.SkippedCalls.filter (fun proc_name _ ->
Purity.should_report proc_name && not (is_modeled_pure tenv proc_name) )
in
@ -177,8 +180,8 @@ let checker {exe_env; Callbacks.summary} : Summary.t =
impure_fun_desc
| Some pre_posts ->
let (ImpurityDomain.{modified_globals; modified_params; skipped_calls} as impurity_astate) =
List.fold pre_posts ~init:ImpurityDomain.pure ~f:(fun acc pre_post ->
let modified = extract_impurity tenv pdesc pre_post in
List.fold pre_posts ~init:ImpurityDomain.pure ~f:(fun acc exec_state ->
let modified = extract_impurity tenv pdesc exec_state in
ImpurityDomain.join acc modified )
in
if Purity.should_report proc_name && not (ImpurityDomain.is_pure impurity_astate) then

@ -27,6 +27,10 @@ let check_error summary = function
raise_notrace AbstractDomain.Stop_analysis
let check_error_continue summary result =
PulseExecutionState.ContinueProgram (check_error summary result)
let proc_name_of_call call_exp =
match (call_exp : Exp.t) with
| Const (Cfun proc_name) | Closure {name= proc_name} ->
@ -39,7 +43,7 @@ type get_formals = Procname.t -> (Pvar.t * Typ.t) list option
module PulseTransferFunctions = struct
module CFG = ProcCfg.Normal
module Domain = PulseAbductiveDomain
module Domain = PulseExecutionState
type extras = get_formals
@ -50,9 +54,9 @@ module PulseTransferFunctions = struct
PulseOperations.call ~caller_summary call_loc callee_pname ~ret ~actuals ~formals_opt astate
| _ ->
L.d_printfln "Skipping indirect call %a@\n" Exp.pp call_exp ;
Ok
[ PulseOperations.unknown_call call_loc (SkippedUnknownCall call_exp) ~ret ~actuals
~formals_opt:None astate ]
PulseOperations.unknown_call call_loc (SkippedUnknownCall call_exp) ~ret ~actuals
~formals_opt:None astate
|> PulseOperations.ok_continue
(** has an object just gone out of scope? *)
@ -70,11 +74,16 @@ module PulseTransferFunctions = struct
(** [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) astate =
let exec_object_out_of_scope call_loc (pvar, typ) exec_state =
match exec_state with
| PulseExecutionState.ContinueProgram astate ->
let gone_out_of_scope = Invalidation.GoneOutOfScope (pvar, typ) in
(* invalidate [&x] *)
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
>>| PulseExecutionState.continue
| PulseExecutionState.ExitProgram _ ->
Ok exec_state
let dispatch_call tenv summary ret call_exp actuals call_loc flags get_formals astate =
@ -99,7 +108,7 @@ module PulseTransferFunctions = struct
None
in
(* do interprocedural call then destroy objects going out of scope *)
let posts =
let exec_state_res =
match model with
| Some (model, callee_procname) ->
L.d_printfln "Found model for call@\n" ;
@ -121,15 +130,21 @@ module PulseTransferFunctions = struct
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* posts = posts in
List.map posts ~f:(fun astate -> exec_object_out_of_scope call_loc pvar_typ astate)
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 ->
posts
exec_state_res
let exec_instr (astate : Domain.t) {tenv; ProcData.summary; extras= get_formals} _cfg_node
(instr : Sil.instr) =
(instr : Sil.instr) : Domain.t list =
match astate with
| 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] *)
@ -137,7 +152,7 @@ module PulseTransferFunctions = struct
let+ astate, rhs_addr_hist = PulseOperations.eval_deref loc rhs_exp astate in
PulseOperations.write_id lhs_id rhs_addr_hist astate
in
[check_error summary result]
[check_error_continue summary result]
| Store {e1= lhs_exp; e2= rhs_exp; loc} ->
(* [*lhs_exp := rhs_exp] *)
let event = ValueHistory.Assignment loc in
@ -156,24 +171,26 @@ module PulseTransferFunctions = struct
| _ ->
Ok astate
in
[check_error summary result]
[check_error_continue summary result]
| Prune (condition, loc, is_then_branch, if_kind) ->
let post, cond_satisfiable =
PulseOperations.prune ~is_then_branch if_kind loc ~condition astate |> check_error summary
let exec_state, cond_satisfiable =
PulseOperations.prune ~is_then_branch if_kind loc ~condition astate
|> check_error summary
in
if cond_satisfiable then (* [condition] is true or unknown value: go into the branch *)
[post]
if cond_satisfiable then
(* [condition] is true or unknown value: go into the branch *)
[Domain.continue exec_state]
else (* [condition] is known to be unsatisfiable: prune path *) []
| Call (ret, call_exp, actuals, loc, call_flags) ->
dispatch_call tenv summary ret call_exp actuals loc call_flags get_formals astate
|> check_error summary
| Metadata (ExitScope (vars, location)) ->
let astate = PulseOperations.remove_vars vars location astate in
[check_error summary astate]
[check_error_continue summary astate]
| Metadata (VariableLifetimeBegins (pvar, _, location)) ->
[PulseOperations.realloc_pvar pvar location astate]
[PulseOperations.realloc_pvar pvar location astate |> Domain.continue]
| Metadata (Abstract _ | Nullify _ | Skip) ->
[astate]
[Domain.ContinueProgram astate] )
let pp_session_name _node fmt = F.pp_print_string fmt "Pulse"
@ -197,7 +214,7 @@ let checker {Callbacks.exe_env; summary} =
AbstractValue.init () ;
let pdesc = Summary.get_proc_desc summary in
let initial =
DisjunctiveTransferFunctions.Disjuncts.singleton (PulseAbductiveDomain.mk_initial pdesc)
DisjunctiveTransferFunctions.Disjuncts.singleton (PulseExecutionState.mk_initial pdesc)
in
let get_formals callee_pname =
Ondemand.get_proc_desc callee_pname |> Option.map ~f:Procdesc.get_pvar_formals

@ -362,11 +362,6 @@ let set_post_cell (addr, history) (edges_map, attr_set) location astate =
|> BaseAddressAttributes.add addr attr_set )
module PrePost = struct
type domain_t = t
type t = domain_t
let filter_for_summary astate =
let post_stack =
BaseStack.filter
@ -430,7 +425,7 @@ module PrePost = struct
(** stuff we carry around when computing the result of applying one pre/post pair *)
type call_state =
{ astate: domain_t (** caller's abstract state computed so far *)
{ astate: t (** caller's abstract state computed so far *)
; subst: (AbstractValue.t * ValueHistory.t) AddressMap.t
(** translation from callee addresses to caller addresses and their caller histories *)
; rev_subst: AbstractValue.t AddressMap.t
@ -463,8 +458,8 @@ module PrePost = struct
; call_state: call_state }
(** raised when the precondition and the current state disagree on the aliasing, i.e. some
addresses [callee_addr] and [callee_addr'] that are distinct in the pre are aliased to a
single address [caller_addr] in the caller's current state. Typically raised when
calling [foo(z,z)] where the spec for [foo(x,y)] says that [x] and [y] are disjoint. *)
single address [caller_addr] in the caller's current state. Typically raised when calling
[foo(z,z)] where the spec for [foo(x,y)] says that [x] and [y] are disjoint. *)
| CItv of
{ addr_caller: AbstractValue.t
; addr_callee: AbstractValue.t
@ -493,8 +488,8 @@ module PrePost = struct
F.fprintf fmt
"caller addr %a%a but callee addr %a%a; %a=%a is unsatisfiable@\n\
note: current call state was %a" AbstractValue.pp addr_caller (Pp.option CItv.pp)
arith_caller AbstractValue.pp addr_callee (Pp.option CItv.pp) arith_callee
AbstractValue.pp addr_caller AbstractValue.pp addr_callee pp_call_state call_state
arith_caller AbstractValue.pp addr_callee (Pp.option CItv.pp) arith_callee AbstractValue.pp
addr_caller AbstractValue.pp addr_callee pp_call_state call_state
| ArithmeticBo {addr_caller; addr_callee; arith_callee; call_state} ->
F.fprintf fmt
"callee addr %a%a is incompatible with caller addr %a's arithmetic constraints@\n\
@ -555,8 +550,8 @@ module PrePost = struct
(** Materialize the (abstract memory) subgraph of [pre] reachable from [addr_pre] in
[call_state.astate] starting from address [addr_caller]. Report an error if some invalid
addresses are traversed in the process. *)
let rec materialize_pre_from_address callee_proc_name call_location ~pre ~addr_pre
~addr_hist_caller call_state =
let rec materialize_pre_from_address callee_proc_name call_location ~pre ~addr_pre ~addr_hist_caller
call_state =
match visit call_state ~addr_callee:addr_pre ~addr_hist_caller with
| `AlreadyVisited, call_state ->
Ok call_state
@ -565,20 +560,18 @@ module PrePost = struct
| None ->
Ok call_state
| Some edges_pre ->
Container.fold_result
~fold:(IContainer.fold_of_pervasives_map_fold ~fold:Memory.Edges.fold)
Container.fold_result ~fold:(IContainer.fold_of_pervasives_map_fold ~fold:Memory.Edges.fold)
~init:call_state edges_pre ~f:(fun call_state (access, (addr_pre_dest, _)) ->
let astate, addr_hist_dest_caller =
Memory.eval_edge addr_hist_caller access call_state.astate
in
let call_state = {call_state with astate} in
materialize_pre_from_address callee_proc_name call_location ~pre
~addr_pre:addr_pre_dest ~addr_hist_caller:addr_hist_dest_caller call_state ) )
materialize_pre_from_address callee_proc_name call_location ~pre ~addr_pre:addr_pre_dest
~addr_hist_caller:addr_hist_dest_caller call_state ) )
(** materialize subgraph of [pre] rooted at the address represented by a [formal] parameter that
has been instantiated with the corresponding [actual] into the current state
[call_state.astate] *)
(** materialize subgraph of [pre] rooted at the address represented by a [formal] parameter that has
been instantiated with the corresponding [actual] into the current state [call_state.astate] *)
let materialize_pre_from_actual callee_proc_name call_location ~pre ~formal ~actual call_state =
L.d_printfln "Materializing PRE from [%a <- %a]" Var.pp formal AbstractValue.pp (fst actual) ;
(let open IOption.Let_syntax in
@ -673,8 +666,7 @@ module PrePost = struct
Attribute.BoItv itv'
| Bottom ->
raise
(Contradiction (ArithmeticBo {addr_callee; addr_caller; arith_callee= itv; call_state}))
)
(Contradiction (ArithmeticBo {addr_callee; addr_caller; arith_callee= itv; call_state})) )
| AddressOfCppTemporary _
| AddressOfStackVariable _
| Allocated _
@ -722,8 +714,8 @@ module PrePost = struct
let apply_arithmetic_constraints callee_proc_name call_location pre_post call_state =
let one_address_sat addr_callee callee_attrs (addr_caller, caller_history) call_state =
let subst, attrs_caller =
add_call_to_attributes callee_proc_name call_location ~addr_callee ~addr_caller
caller_history callee_attrs call_state
add_call_to_attributes callee_proc_name call_location ~addr_callee ~addr_caller caller_history
callee_attrs call_state
in
let astate = AddressAttributes.abduce_and_add addr_caller attrs_caller call_state.astate in
if phys_equal subst call_state.subst && phys_equal astate call_state.astate then call_state
@ -805,9 +797,7 @@ module PrePost = struct
add_call_to_attributes ~addr_callee ~addr_caller callee_proc_name call_loc hist_caller
attrs_post call_state
in
let astate =
AddressAttributes.abduce_and_add addr_caller attrs_post_caller call_state.astate
in
let astate = AddressAttributes.abduce_and_add addr_caller attrs_post_caller call_state.astate in
{call_state with subst; astate}
in
let heap = (call_state.astate.post :> base_domain).heap in
@ -866,14 +856,12 @@ module PrePost = struct
| None ->
call_state
| Some ((edges_post, _attrs_post) as cell_post) ->
let edges_pre_opt =
BaseMemory.find_opt addr_callee (pre :> BaseDomain.t).BaseDomain.heap
in
let edges_pre_opt = BaseMemory.find_opt addr_callee (pre :> BaseDomain.t).BaseDomain.heap in
let call_state_after_post =
if is_cell_read_only ~edges_pre_opt ~cell_post then call_state
else
record_post_cell callee_proc_name call_loc ~addr_callee ~edges_pre_opt
~addr_hist_caller ~cell_post call_state
record_post_cell callee_proc_name call_loc ~addr_callee ~edges_pre_opt ~addr_hist_caller
~cell_post call_state
in
IContainer.fold_of_pervasives_map_fold ~fold:Memory.Edges.fold ~init:call_state_after_post
edges_post ~f:(fun call_state (_access, (addr_callee_dest, _)) ->
@ -881,13 +869,12 @@ module PrePost = struct
call_state_subst_find_or_new call_state addr_callee_dest
~default_hist_caller:(snd addr_hist_caller)
in
record_post_for_address callee_proc_name call_loc pre_post
~addr_callee:addr_callee_dest ~addr_hist_caller:addr_hist_curr_dest call_state ) )
record_post_for_address callee_proc_name call_loc pre_post ~addr_callee:addr_callee_dest
~addr_hist_caller:addr_hist_curr_dest call_state ) )
let record_post_for_actual callee_proc_name call_loc pre_post ~formal ~actual call_state =
L.d_printfln_escaped "Recording POST from [%a] <-> %a" Var.pp formal AbstractValue.pp
(fst actual) ;
L.d_printfln_escaped "Recording POST from [%a] <-> %a" Var.pp formal AbstractValue.pp (fst actual) ;
match
let open IOption.Let_syntax in
let* addr_formal_pre, _ =
@ -934,8 +921,7 @@ module PrePost = struct
(call_state, Some return_caller_addr_hist) )
let apply_post_for_parameters callee_proc_name call_location pre_post ~formals ~actuals call_state
=
let apply_post_for_parameters callee_proc_name call_location pre_post ~formals ~actuals call_state =
(* for each [(formal_i, actual_i)] pair, do [post_i = post union subst(graph reachable from
formal_i in post)], deleting previous info when comparing pre and post shows a difference
(TODO: record in the pre when a location is written to instead of just comparing values
@ -943,8 +929,7 @@ module PrePost = struct
post but nuke other fields in the meantime? is that possible?). *)
match
List.fold2 formals actuals ~init:call_state ~f:(fun call_state formal (actual, _) ->
record_post_for_actual callee_proc_name call_location pre_post ~formal ~actual call_state
)
record_post_for_actual callee_proc_name call_location pre_post ~formal ~actual call_state )
with
| Unequal_lengths ->
(* should have been checked before by [materialize_pre] *)
@ -1038,8 +1023,8 @@ module PrePost = struct
AddressAttributes.check_valid access_trace addr_caller astate
|> Result.map_error ~f:(fun (invalidation, invalidation_trace) ->
L.d_printfln "ERROR: caller's %a invalid!" AbstractValue.pp addr_caller ;
Diagnostic.AccessToInvalidAddress
{invalidation; invalidation_trace; access_trace} ) ) )
Diagnostic.AccessToInvalidAddress {invalidation; invalidation_trace; access_trace}
) ) )
call_state.subst (Ok call_state.astate)
@ -1099,4 +1084,3 @@ module PrePost = struct
let get_post {post} = (post :> BaseDomain.t)
let get_skipped_calls {skipped_calls} = skipped_calls
end

@ -110,14 +110,17 @@ val discard_unreachable : t -> t * BaseAddressAttributes.t
val add_skipped_calls : Procname.t -> PulseTrace.t -> t -> t
module PrePost : sig
type domain_t = t
type t = private domain_t
val leq : lhs:t -> rhs:t -> bool
val pp : Format.formatter -> t -> unit
val of_post : Procdesc.t -> domain_t -> t
val of_post : Procdesc.t -> t -> t
val get_pre : t -> BaseDomain.t
val get_post : t -> BaseDomain.t
val get_skipped_calls : t -> SkippedCalls.t
val apply :
Procname.t
@ -125,14 +128,7 @@ module PrePost : sig
-> t
-> formals:Var.t list
-> actuals:((AbstractValue.t * ValueHistory.t) * Typ.t) list
-> domain_t
-> ((domain_t * (AbstractValue.t * ValueHistory.t) option) option, Diagnostic.t) result
-> t
-> ((t * (AbstractValue.t * ValueHistory.t) option) option, Diagnostic.t) result
(** return the abstract state after the call along with an optional return value, or [None] if the
precondition could not be satisfied (e.g. some aliasing constraints were not satisfied) *)
val get_pre : t -> BaseDomain.t
val get_post : t -> BaseDomain.t
val get_skipped_calls : t -> SkippedCalls.t
end

@ -0,0 +1,43 @@
(*
* 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
type exec_state =
| ContinueProgram of PulseAbductiveDomain.t
| ExitProgram of PulseAbductiveDomain.t
type t = exec_state
let continue astate = ContinueProgram astate
let mk_initial pdesc = ContinueProgram (PulseAbductiveDomain.mk_initial pdesc)
let leq ~lhs ~rhs =
match (lhs, rhs) with
| ContinueProgram astate1, ContinueProgram astate2 | ExitProgram astate1, ExitProgram astate2 ->
PulseAbductiveDomain.leq ~lhs:astate1 ~rhs:astate2
| ExitProgram _, ContinueProgram _ | ContinueProgram _, ExitProgram _ ->
false
let pp fmt = function
| ContinueProgram astate ->
PulseAbductiveDomain.pp fmt astate
| ExitProgram astate ->
F.fprintf fmt "{ExitProgram %a}" PulseAbductiveDomain.pp astate
let map ~f exec_state =
match exec_state with
| ContinueProgram astate ->
ContinueProgram (f astate)
| ExitProgram astate ->
ExitProgram (f astate)
let of_post pdesc = map ~f:(PulseAbductiveDomain.of_post pdesc)

@ -0,0 +1,20 @@
(*
* 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
type exec_state =
| ContinueProgram of PulseAbductiveDomain.t (** represents the state at the program point *)
| ExitProgram of PulseAbductiveDomain.t
(** represents the state originating at exit/divergence. *)
include AbstractDomain.NoJoin with type t = exec_state
val continue : PulseAbductiveDomain.t -> t
val of_post : Procdesc.t -> t -> t
val mk_initial : Procdesc.t -> t

@ -17,7 +17,7 @@ type model =
-> Location.t
-> ret:Ident.t * Typ.t
-> PulseAbductiveDomain.t
-> PulseAbductiveDomain.t list PulseOperations.access_result
-> PulseExecutionState.t list PulseOperations.access_result
module Misc = struct
let shallow_copy model_desc dest_pointer_hist src_pointer_hist : model =
@ -30,10 +30,14 @@ module Misc = struct
~obj:(fst obj_copy, event :: snd obj_copy)
astate
in
[PulseOperations.havoc_id ret_id [event] astate]
let astate = PulseOperations.havoc_id ret_id [event] astate in
[PulseExecutionState.ContinueProgram astate]
let early_exit : model =
fun ~caller_summary:_ ~callee_procname:_ _ ~ret:_ astate ->
Ok [PulseExecutionState.ExitProgram astate]
let early_exit : model = fun ~caller_summary:_ ~callee_procname:_ _ ~ret:_ _ -> Ok []
let return_int : Int64.t -> model =
fun i64 ~caller_summary:_ ~callee_procname:_ location ~ret:(ret_id, _) astate ->
@ -44,7 +48,7 @@ module Misc = struct
|> AddressAttributes.add_one ret_addr
(CItv (CItv.equal_to i, Immediate {location; history= []}))
in
Ok [PulseOperations.write_id ret_id (ret_addr, []) astate]
PulseOperations.write_id ret_id (ret_addr, []) astate |> PulseOperations.ok_continue
let return_unknown_size : model =
@ -55,20 +59,22 @@ module Misc = struct
|> AddressAttributes.add_one ret_addr
(CItv (CItv.zero_inf, Immediate {location; history= []}))
in
Ok [PulseOperations.write_id ret_id (ret_addr, []) astate]
PulseOperations.write_id ret_id (ret_addr, []) astate |> PulseOperations.ok_continue
let skip : model =
fun ~caller_summary:_ ~callee_procname:_ _ ~ret:_ astate -> PulseOperations.ok_continue astate
let skip : model = fun ~caller_summary:_ ~callee_procname:_ _ ~ret:_ astate -> Ok [astate]
let nondet ~fn_name : model =
fun ~caller_summary:_ ~callee_procname:_ location ~ret:(ret_id, _) astate ->
let event = ValueHistory.Call {f= Model fn_name; location; in_call= []} in
Ok [PulseOperations.havoc_id ret_id [event] astate]
PulseOperations.havoc_id ret_id [event] astate |> PulseOperations.ok_continue
let id_first_arg arg_access_hist : model =
fun ~caller_summary:_ ~callee_procname:_ _ ~ret astate ->
Ok [PulseOperations.write_id (fst ret) arg_access_hist astate]
PulseOperations.write_id (fst ret) arg_access_hist astate |> PulseOperations.ok_continue
end
module C = struct
@ -82,10 +88,10 @@ module C = struct
|| Itv.ItvPure.is_zero (AddressAttributes.get_bo_itv (fst deleted_access) astate)
in
if is_known_zero then (* freeing 0 is a no-op *)
Ok [astate]
PulseOperations.ok_continue astate
else
let+ astate = PulseOperations.invalidate location Invalidation.CFree deleted_access astate in
[astate]
[PulseExecutionState.ContinueProgram astate]
let malloc _ : model =
@ -97,6 +103,7 @@ module C = struct
|> AddressAttributes.add_one ret_addr (BoItv Itv.ItvPure.pos)
|> AddressAttributes.add_one ret_addr
(CItv (CItv.ge_to IntLit.one, Immediate {location; history= []}))
|> PulseExecutionState.continue
in
let+ astate_null =
AddressAttributes.add_one ret_addr (BoItv (Itv.ItvPure.of_int_lit IntLit.zero)) astate
@ -106,23 +113,27 @@ module C = struct
|> PulseOperations.invalidate location (Invalidation.ConstantDereference IntLit.zero)
(ret_addr, [])
in
[astate_alloc; astate_null]
[astate_alloc; PulseExecutionState.ContinueProgram astate_null]
end
module Cplusplus = struct
let delete deleted_access : model =
fun ~caller_summary:_ ~callee_procname:_ location ~ret:_ astate ->
PulseOperations.invalidate location Invalidation.CppDelete deleted_access astate >>| List.return
let+ astate =
PulseOperations.invalidate location Invalidation.CppDelete deleted_access astate
in
[PulseExecutionState.ContinueProgram astate]
let placement_new actuals : model =
fun ~caller_summary:_ ~callee_procname:_ location ~ret:(ret_id, _) astate ->
let event = ValueHistory.Call {f= Model "<placement new>()"; location; in_call= []} in
match List.rev actuals with
( match List.rev actuals with
| ProcnameDispatcher.Call.FuncArg.{arg_payload= address, hist} :: _ ->
Ok [PulseOperations.write_id ret_id (address, event :: hist) astate]
PulseOperations.write_id ret_id (address, event :: hist) astate
| _ ->
Ok [PulseOperations.havoc_id ret_id [event] astate]
PulseOperations.havoc_id ret_id [event] astate )
|> PulseOperations.ok_continue
end
module StdAtomicInteger = struct
@ -150,7 +161,7 @@ module StdAtomicInteger = struct
in
let* astate = PulseOperations.write_deref location ~ref:int_field ~obj:init_value astate in
let+ astate = PulseOperations.write_deref location ~ref:this_address ~obj:this astate in
[astate]
[PulseExecutionState.ContinueProgram astate]
let arith_bop prepost location event ret_id bop this operand astate =
@ -173,7 +184,7 @@ module StdAtomicInteger = struct
arith_bop `Post location event ret_id (PlusA None) this (AbstractValueOperand increment)
astate
in
[astate]
[PulseExecutionState.ContinueProgram astate]
let fetch_sub this (increment, _) _memory_ordering : model =
@ -183,7 +194,7 @@ module StdAtomicInteger = struct
arith_bop `Post location event ret_id (MinusA None) this (AbstractValueOperand increment)
astate
in
[astate]
[PulseExecutionState.ContinueProgram astate]
let operator_plus_plus_pre this : model =
@ -192,7 +203,7 @@ module StdAtomicInteger = struct
let+ astate =
arith_bop `Pre location event ret_id (PlusA None) this (LiteralOperand IntLit.one) astate
in
[astate]
[PulseExecutionState.ContinueProgram astate]
let operator_plus_plus_post this _int : model =
@ -203,7 +214,7 @@ module StdAtomicInteger = struct
let+ astate =
arith_bop `Post location event ret_id (PlusA None) this (LiteralOperand IntLit.one) astate
in
[astate]
[PulseExecutionState.ContinueProgram astate]
let operator_minus_minus_pre this : model =
@ -212,7 +223,7 @@ module StdAtomicInteger = struct
let+ astate =
arith_bop `Pre location event ret_id (MinusA None) this (LiteralOperand IntLit.one) astate
in
[astate]
[PulseExecutionState.ContinueProgram astate]
let operator_minus_minus_post this _int : model =
@ -223,14 +234,15 @@ module StdAtomicInteger = struct
let+ astate =
arith_bop `Post location event ret_id (MinusA None) this (LiteralOperand IntLit.one) astate
in
[astate]
[PulseExecutionState.ContinueProgram astate]
let load_instr model_desc this _memory_ordering_opt : model =
fun ~caller_summary:_ ~callee_procname:_ location ~ret:(ret_id, _) astate ->
let event = ValueHistory.Call {f= Model model_desc; location; in_call= []} in
let+ astate, _int_addr, (int, hist) = load_backing_int location this astate in
[PulseOperations.write_id ret_id (int, event :: hist) astate]
let astate = PulseOperations.write_id ret_id (int, event :: hist) astate in
[PulseExecutionState.ContinueProgram astate]
let load = load_instr "std::atomic<T>::load()"
@ -249,7 +261,7 @@ module StdAtomicInteger = struct
fun ~caller_summary:_ ~callee_procname:_ location ~ret:_ astate ->
let event = ValueHistory.Call {f= Model "std::atomic::store()"; location; in_call= []} in
let+ astate = store_backing_int location this_address (new_value, event :: new_hist) astate in
[astate]
[PulseExecutionState.ContinueProgram astate]
let exchange this_address (new_value, new_hist) _memory_ordering : model =
@ -257,7 +269,8 @@ module StdAtomicInteger = struct
let event = ValueHistory.Call {f= Model "std::atomic::exchange()"; location; in_call= []} in
let* astate, _int_addr, (old_int, old_hist) = load_backing_int location this_address astate in
let+ astate = store_backing_int location this_address (new_value, event :: new_hist) astate in
[PulseOperations.write_id ret_id (old_int, event :: old_hist) astate]
let astate = PulseOperations.write_id ret_id (old_int, event :: old_hist) astate in
[PulseExecutionState.ContinueProgram astate]
end
module ObjectiveC = struct
@ -268,7 +281,7 @@ module ObjectiveC = struct
in
let ret_addr = AbstractValue.mk_fresh () in
let astate = PulseOperations.allocate callee_procname location (ret_addr, []) astate in
Ok [PulseOperations.write_id ret_id (ret_addr, hist) astate]
PulseOperations.write_id ret_id (ret_addr, hist) astate |> PulseOperations.ok_continue
end
module JavaObject = struct
@ -278,7 +291,8 @@ module JavaObject = struct
let event = ValueHistory.Call {f= Model "Object.clone"; location; in_call= []} in
let* astate, obj = PulseOperations.eval_access location src_pointer_hist Dereference astate in
let+ astate, obj_copy = PulseOperations.shallow_copy location obj astate in
[PulseOperations.write_id ret_id (fst obj_copy, event :: snd obj_copy) astate]
let astate = PulseOperations.write_id ret_id (fst obj_copy, event :: snd obj_copy) astate in
[PulseExecutionState.ContinueProgram astate]
end
module StdBasicString = struct
@ -301,7 +315,8 @@ module StdBasicString = struct
let+ astate, (string, hist) =
PulseOperations.eval_access location string_addr_hist Dereference astate
in
[PulseOperations.write_id ret_id (string, event :: hist) astate]
let astate = PulseOperations.write_id ret_id (string, event :: hist) astate in
[PulseExecutionState.ContinueProgram astate]
let destructor this_hist : model =
@ -312,7 +327,7 @@ module StdBasicString = struct
let string_addr_hist = (string_addr, call_event :: string_hist) in
let* astate = PulseOperations.invalidate_deref location CppDelete string_addr_hist astate in
let+ astate = PulseOperations.invalidate location CppDelete string_addr_hist astate in
[astate]
[PulseExecutionState.ContinueProgram astate]
end
module StdFunction = struct
@ -328,7 +343,8 @@ module StdFunction = struct
let* astate = PulseOperations.Closures.check_captured_addresses location lambda astate in
match AddressAttributes.get_closure_proc_name lambda astate with
| None ->
(* we don't know what proc name this lambda resolves to *) Ok (havoc_ret ret astate)
(* we don't know what proc name this lambda resolves to *)
Ok (havoc_ret ret astate |> List.map ~f:PulseExecutionState.continue)
| Some callee_proc_name ->
let actuals =
List.map actuals ~f:(fun ProcnameDispatcher.Call.FuncArg.{arg_payload; typ} ->
@ -373,14 +389,16 @@ module StdVector = struct
; location
; in_call= [] }
in
reallocate_internal_array [crumb] vector vector_f location astate >>| List.return
reallocate_internal_array [crumb] vector vector_f location astate
>>| PulseExecutionState.continue >>| List.return
let at ~desc vector index : model =
fun ~caller_summary:_ ~callee_procname:_ location ~ret astate ->
let event = ValueHistory.Call {f= Model desc; location; in_call= []} in
let+ astate, (addr, hist) = element_of_internal_array location vector (fst index) astate in
[PulseOperations.write_id (fst ret) (addr, event :: hist) astate]
let astate = PulseOperations.write_id (fst ret) (addr, event :: hist) astate in
[PulseExecutionState.ContinueProgram astate]
let reserve vector : model =
@ -388,7 +406,7 @@ module StdVector = struct
let crumb = ValueHistory.Call {f= Model "std::vector::reserve()"; location; in_call= []} in
reallocate_internal_array [crumb] vector Reserve location astate
>>| AddressAttributes.std_vector_reserve (fst vector)
>>| List.return
>>| PulseExecutionState.continue >>| List.return
let push_back vector : model =
@ -397,10 +415,11 @@ module StdVector = struct
if AddressAttributes.is_std_vector_reserved (fst vector) astate then
(* assume that any call to [push_back] is ok after one called [reserve] on the same vector
(a perfect analysis would also make sure we don't exceed the reserved size) *)
Ok [astate]
PulseOperations.ok_continue astate
else
(* simulate a re-allocation of the underlying array every time an element is added *)
reallocate_internal_array [crumb] vector PushBack location astate >>| List.return
reallocate_internal_array [crumb] vector PushBack location astate
>>| PulseExecutionState.continue >>| List.return
end
module JavaCollection = struct
@ -416,7 +435,8 @@ module JavaCollection = struct
astate
>>= PulseOperations.invalidate_deref location (StdVector Assign) old_elem
in
[PulseOperations.write_id (fst ret) (old_addr, event :: old_hist) astate]
let astate = PulseOperations.write_id (fst ret) (old_addr, event :: old_hist) astate in
[PulseExecutionState.ContinueProgram astate]
end
module StringSet = Caml.Set.Make (String)

@ -13,7 +13,7 @@ type model =
-> Location.t
-> ret:Ident.t * Typ.t
-> PulseAbductiveDomain.t
-> PulseAbductiveDomain.t list PulseOperations.access_result
-> PulseExecutionState.t list PulseOperations.access_result
val dispatch :
Tenv.t

@ -14,6 +14,8 @@ type t = AbductiveDomain.t
type 'a access_result = ('a, Diagnostic.t) result
let ok_continue post = Ok [PulseExecutionState.ContinueProgram post]
(** Check that the [address] is not known to be invalid *)
let check_addr_access location (address, history) astate =
let access_trace = Trace.Immediate {location; history} in
@ -570,24 +572,14 @@ let unknown_call call_loc reason ~ret ~actuals ~formals_opt astate =
|> havoc_ret ret |> add_skipped_proc
let call ~caller_summary call_loc callee_pname ~ret ~actuals ~formals_opt astate =
match PulsePayload.read_full ~caller_summary ~callee_pname with
| Some (callee_proc_desc, preposts) ->
let formals =
Procdesc.get_formals callee_proc_desc
|> List.map ~f:(fun (mangled, _) -> Pvar.mk mangled callee_pname |> Var.of_pvar)
in
(* call {!AbductiveDomain.PrePost.apply} on each pre/post pair in the summary. *)
List.fold_result preposts ~init:[] ~f:(fun posts pre_post ->
(* apply all pre/post specs *)
AbductiveDomain.PrePost.apply callee_pname call_loc pre_post ~formals ~actuals astate
let apply_callee callee_pname call_loc callee_exec_state ~ret ~formals ~actuals astate =
let apply callee_prepost ~f =
PulseAbductiveDomain.apply callee_pname call_loc callee_prepost ~formals ~actuals astate
>>| function
| None ->
(* couldn't apply pre/post pair *) posts
(* couldn't apply pre/post pair *) None
| Some (post, return_val_opt) ->
let event =
ValueHistory.Call {f= Call callee_pname; location= call_loc; in_call= []}
in
let event = ValueHistory.Call {f= Call callee_pname; location= call_loc; in_call= []} in
let post =
match return_val_opt with
| Some (return_val, return_hist) ->
@ -595,8 +587,32 @@ let call ~caller_summary call_loc callee_pname ~ret ~actuals ~formals_opt astate
| None ->
havoc_id (fst ret) [event] post
in
post :: posts )
Some (f post)
in
let open PulseExecutionState in
match callee_exec_state with
| ContinueProgram astate ->
apply astate ~f:(fun astate -> ContinueProgram astate)
| ExitProgram astate ->
apply astate ~f:(fun astate -> ExitProgram astate)
let call ~caller_summary call_loc callee_pname ~ret ~actuals ~formals_opt
(astate : PulseAbductiveDomain.t) : (PulseExecutionState.t list, Diagnostic.t) result =
match PulsePayload.read_full ~caller_summary ~callee_pname with
| Some (callee_proc_desc, exec_states) ->
let formals =
Procdesc.get_formals callee_proc_desc
|> List.map ~f:(fun (mangled, _) -> Pvar.mk mangled callee_pname |> Var.of_pvar)
in
(* call {!AbductiveDomain.PrePost.apply} on each pre/post pair in the summary. *)
List.fold_result exec_states ~init:[] ~f:(fun posts callee_exec_state ->
(* apply all pre/post specs *)
apply_callee callee_pname call_loc callee_exec_state ~formals ~actuals ~ret astate
>>| function
| None -> (* couldn't apply pre/post pair *) posts | Some post -> post :: posts )
| None ->
(* no spec found for some reason (unknown function, ...) *)
L.d_printfln "No spec found for %a@\n" Procname.pp callee_pname ;
Ok [unknown_call call_loc (SkippedKnownCall callee_pname) ~ret ~actuals ~formals_opt astate]
unknown_call call_loc (SkippedKnownCall callee_pname) ~ret ~actuals ~formals_opt astate
|> ok_continue

@ -13,6 +13,8 @@ type t = PulseAbductiveDomain.t
type 'a access_result = ('a, Diagnostic.t) result
val ok_continue : t -> (PulseExecutionState.exec_state list, 'a) result
module Closures : sig
val check_captured_addresses : Location.t -> AbstractValue.t -> t -> (t, Diagnostic.t) result
(** assert the validity of the addresses captured by the lambda *)
@ -112,7 +114,7 @@ val call :
-> actuals:((AbstractValue.t * ValueHistory.t) * Typ.t) list
-> formals_opt:(Pvar.t * Typ.t) list option
-> t
-> t list access_result
-> PulseExecutionState.t list access_result
(** perform an interprocedural call: apply the summary for the call proc name passed as argument if
it exists *)

@ -6,15 +6,14 @@
*)
open! IStd
module F = Format
open PulseDomainInterface
type t = AbductiveDomain.PrePost.t list
type t = PulseExecutionState.t list
let of_posts pdesc posts = List.map posts ~f:(PulseAbductiveDomain.PrePost.of_post pdesc)
let of_posts pdesc posts = List.map posts ~f:(PulseExecutionState.of_post pdesc)
let pp fmt summary =
F.open_vbox 0 ;
F.fprintf fmt "%d pre/post(s)@;" (List.length summary) ;
List.iteri summary ~f:(fun i pre_post ->
F.fprintf fmt "#%d: @[%a@]@;" i PulseAbductiveDomain.PrePost.pp pre_post ) ;
F.fprintf fmt "#%d: @[%a@]@;" i PulseExecutionState.pp pre_post ) ;
F.close_box ()

@ -6,8 +6,8 @@
*)
open! IStd
type t = PulseAbductiveDomain.PrePost.t list
type t = PulseExecutionState.t list
val of_posts : Procdesc.t -> PulseAbductiveDomain.t list -> t
val of_posts : Procdesc.t -> PulseExecutionState.t list -> t
val pp : Format.formatter -> t -> unit

@ -0,0 +1,20 @@
/*
* 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.
*/
#include <stdlib.h>
int x;
void exit_positive_impure_FN(int a[10], int b) {
if (b > 0) {
exit(0);
}
}
void unreachable_impure_FN(int a[10], int b) {
exit_positive_impure_FN(a, 10);
x = 9;
}

@ -0,0 +1,29 @@
/*
* 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.
*/
#include <stdlib.h>
// we get two disjuncts one for each branch
void exit_positive(int a[10], int b) {
if (b < 1) {
exit(0);
}
}
void unreachable_double_free_ok(int a[10], int b) {
exit_positive(a, 0);
free(a);
free(a);
}
void store_exit(int* x, bool b) {
if (b) {
*x = 42;
exit(0);
}
}
void store_exit_null_bad(bool b) { store_exit(NULL, b); }

@ -12,6 +12,7 @@ codetoanalyze/cpp/pulse/deduplication.cpp, deduplication::SomeTemplatedClass<int
codetoanalyze/cpp/pulse/deduplication.cpp, deduplication::SomeTemplatedClass<int>::lifetime_error_bad, 2, USE_AFTER_DELETE, no_bucket, ERROR, [invalidation part of the trace starts here,parameter `a` of deduplication::SomeTemplatedClass<int>::lifetime_error_bad,when calling `deduplication::SomeTemplatedClass<int>::templated_wrapper_delete_ok` here,parameter `a` of deduplication::SomeTemplatedClass<int>::templated_wrapper_delete_ok,was invalidated by `delete`,use-after-lifetime part of the trace starts here,parameter `a` of deduplication::SomeTemplatedClass<int>::lifetime_error_bad,when calling `deduplication::SomeTemplatedClass<int>::templated_wrapper_access_ok` here,parameter `a` of deduplication::SomeTemplatedClass<int>::templated_wrapper_access_ok,invalid access occurs here]
codetoanalyze/cpp/pulse/deduplication.cpp, deduplication::templated_function_bad<_Bool>, 3, USE_AFTER_DELETE, no_bucket, ERROR, [invalidation part of the trace starts here,assigned,when calling `deduplication::templated_delete_function<_Bool>` here,parameter `a` of deduplication::templated_delete_function<_Bool>,was invalidated by `delete`,use-after-lifetime part of the trace starts here,assigned,when calling `deduplication::templated_access_function<_Bool>` here,parameter `a` of deduplication::templated_access_function<_Bool>,invalid access occurs here]
codetoanalyze/cpp/pulse/deduplication.cpp, deduplication::templated_function_bad<int>, 3, USE_AFTER_DELETE, no_bucket, ERROR, [invalidation part of the trace starts here,assigned,when calling `deduplication::templated_delete_function<int>` here,parameter `a` of deduplication::templated_delete_function<int>,was invalidated by `delete`,use-after-lifetime part of the trace starts here,assigned,when calling `deduplication::templated_access_function<int>` here,parameter `a` of deduplication::templated_access_function<int>,invalid access occurs here]
codetoanalyze/cpp/pulse/exit_test.cpp, store_exit_null_bad, 0, NULLPTR_DEREFERENCE, no_bucket, ERROR, [invalidation part of the trace starts here,assigned,is the null pointer,use-after-lifetime part of the trace starts here,when calling `store_exit` here,parameter `x` of store_exit,invalid access occurs here]
codetoanalyze/cpp/pulse/folly_DestructorGuard.cpp, UsingDelayedDestruction::double_delete_bad, 2, USE_AFTER_DELETE, no_bucket, ERROR, [invalidation part of the trace starts here,parameter `this` of UsingDelayedDestruction::double_delete_bad,was invalidated by `delete`,use-after-lifetime part of the trace starts here,parameter `this` of UsingDelayedDestruction::double_delete_bad,invalid access occurs here]
codetoanalyze/cpp/pulse/frontend.cpp, deref_null_namespace_alias_ptr_bad, 4, USE_AFTER_DELETE, no_bucket, ERROR, [invalidation part of the trace starts here,when calling `some::thing::bad_ptr` here,assigned,was invalidated by `delete`,use-after-lifetime part of the trace starts here,passed as argument to `some::thing::bad_ptr`,return from call to `some::thing::bad_ptr`,assigned,invalid access occurs here]
codetoanalyze/cpp/pulse/interprocedural.cpp, access_to_invalidated_alias2_bad, 3, USE_AFTER_DELETE, no_bucket, ERROR, [invalidation part of the trace starts here,parameter `x` of access_to_invalidated_alias2_bad,assigned,when calling `invalidate_and_set_to_null` here,parameter `x_ptr` of invalidate_and_set_to_null,was invalidated by `delete`,use-after-lifetime part of the trace starts here,parameter `x` of access_to_invalidated_alias2_bad,when calling `wraps_read` here,parameter `x` of wraps_read,when calling `wraps_read_inner` here,parameter `x` of wraps_read_inner,invalid access occurs here]

@ -0,0 +1,35 @@
/*
* 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.
*/
#include <stdlib.h>
int double_free_in_catch_bad_FN(int a[10], int b) {
try {
if (b > 0) {
free(a);
throw(0);
}
} catch (...) {
free(a);
}
return 0;
}
void throw_positive(int a[10], int b) {
if (b > 0) {
free(a);
throw(1);
}
}
int double_free_interproc_bad_FN(int a[10], int b) {
try {
throw_positive(a, 2);
} catch (...) {
free(a);
}
return 0;
}

@ -89,8 +89,15 @@ class Test {
return System.nanoTime();
}
// In pulse, we get 0 disjuncts as a summary, hence consider this as impure
void exit_impure() {
// In pulse, we get Exited summary where pre=post
// TODO: change impurity to track exit as impure
void exit_impure_FN() {
System.exit(1);
}
// In pulse, we get Exited summary where pre=post
void modify_exit_impure(int[] a) {
a[0] = 0;
System.exit(1);
}

@ -34,9 +34,9 @@ codetoanalyze/java/impurity/PurityModeled.java, PurityModeled.write_impure():voi
codetoanalyze/java/impurity/Test.java, Test.Test(int):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.Test(int),global variable `Test` modified here]
codetoanalyze/java/impurity/Test.java, Test.alias_impure(int[],int,int):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.alias_impure(int[],int,int),parameter `array` modified here]
codetoanalyze/java/impurity/Test.java, Test.call_impure_impure(int):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.call_impure_impure(int),when calling `void Test.set_impure(int,int)` here,parameter `this` modified here]
codetoanalyze/java/impurity/Test.java, Test.exit_impure():void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.exit_impure() with empty pulse summary]
codetoanalyze/java/impurity/Test.java, Test.global_array_set_impure(int,int):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.global_array_set_impure(int,int),global variable `Test` modified here]
codetoanalyze/java/impurity/Test.java, Test.local_field_write_impure(Test):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.local_field_write_impure(Test),parameter `x` modified here]
codetoanalyze/java/impurity/Test.java, Test.modify_exit_impure(int[]):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.modify_exit_impure(int[]),parameter `a` modified here]
codetoanalyze/java/impurity/Test.java, Test.parameter_field_write_impure(Test,boolean):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.parameter_field_write_impure(Test,boolean),parameter `test` modified here]
codetoanalyze/java/impurity/Test.java, Test.set_impure(int,int):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.set_impure(int,int),parameter `this` modified here]
codetoanalyze/java/impurity/Test.java, Test.swap_impure(int[],int,int):void, 0, IMPURE_FUNCTION, no_bucket, ERROR, [Impure function void Test.swap_impure(int[],int,int),parameter `array` modified here]

Loading…
Cancel
Save