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.
545 lines
18 KiB
545 lines
18 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
|
|
module MF = MarkupFormatter
|
|
|
|
let pname_pp = MF.wrap_monospaced Typ.Procname.pp
|
|
|
|
module ThreadDomain = struct
|
|
type t = UIThread | AnyThread [@@deriving compare]
|
|
|
|
let top = AnyThread
|
|
|
|
let is_top = function AnyThread -> true | UIThread -> false
|
|
|
|
let join st1 st2 =
|
|
match (st1, st2) with AnyThread, _ | _, AnyThread -> AnyThread | _, _ -> UIThread
|
|
|
|
|
|
let ( <= ) ~lhs ~rhs = match (lhs, rhs) with AnyThread, UIThread -> false | _, _ -> true
|
|
|
|
let widen ~prev ~next ~num_iters:_ = join prev next
|
|
|
|
let pp fmt st =
|
|
(match st with UIThread -> "UIThread" | AnyThread -> "AnyThread") |> F.pp_print_string fmt
|
|
|
|
|
|
(** Can two thread statuses occur in parallel? Only [UIThread, UIThread] is forbidden.
|
|
In addition, this is monotonic wrt the lattice (increasing either argument cannot
|
|
transition from true to false). *)
|
|
let can_run_in_parallel st1 st2 =
|
|
match (st1, st2) with UIThread, UIThread -> false | _, _ -> true
|
|
|
|
|
|
let is_uithread = function UIThread -> true | _ -> false
|
|
|
|
(* If we know that either the caller or the callee is on UIThread, keep it that way. *)
|
|
let integrate_summary ~caller ~callee =
|
|
match (caller, callee) with UIThread, _ | _, UIThread -> UIThread | _, _ -> AnyThread
|
|
end
|
|
|
|
module Lock = struct
|
|
(* TODO (T37174859): change to [HilExp.t] *)
|
|
type t = AccessPath.t
|
|
|
|
type var = Var.t
|
|
|
|
let compare_var = Var.compare_modulo_this
|
|
|
|
(* compare type, base variable modulo this and access list *)
|
|
let compare lock lock' =
|
|
if phys_equal lock lock' then 0
|
|
else [%compare: (var * Typ.t) * AccessPath.access list] lock lock'
|
|
|
|
|
|
let equal = [%compare.equal: t]
|
|
|
|
let pp = AccessPath.pp
|
|
|
|
let owner_class ((_, {Typ.desc}), _) =
|
|
match desc with
|
|
| Typ.Tstruct name | Typ.Tptr ({desc= Tstruct name}, _) ->
|
|
Some name
|
|
| _ ->
|
|
None
|
|
|
|
|
|
let describe fmt lock =
|
|
let pp_owner fmt lock =
|
|
owner_class lock |> Option.iter ~f:(F.fprintf fmt " in %a" (MF.wrap_monospaced Typ.Name.pp))
|
|
in
|
|
F.fprintf fmt "%a%a" (MF.wrap_monospaced pp) lock pp_owner lock
|
|
|
|
|
|
let pp_locks fmt lock = F.fprintf fmt " locks %a" describe lock
|
|
end
|
|
|
|
module Event = struct
|
|
type t =
|
|
| LockAcquire of Lock.t
|
|
| MayBlock of (string * StarvationModels.severity)
|
|
| StrictModeCall of string
|
|
[@@deriving compare]
|
|
|
|
let pp fmt = function
|
|
| LockAcquire lock ->
|
|
F.fprintf fmt "LockAcquire(%a)" Lock.pp lock
|
|
| MayBlock (msg, sev) ->
|
|
F.fprintf fmt "MayBlock(%s, %a)" msg StarvationModels.pp_severity sev
|
|
| StrictModeCall msg ->
|
|
F.fprintf fmt "StrictModeCall(%s)" msg
|
|
|
|
|
|
let describe fmt elem =
|
|
match elem with
|
|
| LockAcquire lock ->
|
|
Lock.pp_locks fmt lock
|
|
| MayBlock (msg, _) ->
|
|
F.pp_print_string fmt msg
|
|
| StrictModeCall msg ->
|
|
F.pp_print_string fmt msg
|
|
|
|
|
|
let make_acquire lock = LockAcquire lock
|
|
|
|
let make_call_descr callee = F.asprintf "calls %a" pname_pp callee
|
|
|
|
let make_blocking_call callee sev =
|
|
let descr = make_call_descr callee in
|
|
MayBlock (descr, sev)
|
|
|
|
|
|
let make_strict_mode_call callee =
|
|
let descr = make_call_descr callee in
|
|
StrictModeCall descr
|
|
end
|
|
|
|
(** A lock acquisition with source location and procname in which it occurs.
|
|
The location & procname are *ignored* for comparisons, and are only for reporting. *)
|
|
module Acquisition = struct
|
|
type t =
|
|
{lock: Lock.t; loc: Location.t [@compare.ignore]; procname: Typ.Procname.t [@compare.ignore]}
|
|
[@@deriving compare]
|
|
|
|
let pp fmt {lock} = Lock.pp_locks fmt lock
|
|
|
|
let make ~procname ~loc lock = {lock; loc; procname}
|
|
|
|
let compare_loc {loc= loc1} {loc= loc2} = Location.compare loc1 loc2
|
|
|
|
let make_trace_step acquisition =
|
|
let description = F.asprintf "%a" pp acquisition in
|
|
Errlog.make_trace_element 0 acquisition.loc description []
|
|
|
|
|
|
let make_dummy lock = {lock; loc= Location.dummy; procname= Typ.Procname.Linters_dummy_method}
|
|
end
|
|
|
|
(** Set of acquisitions; due to order over acquisitions, each lock appears at most once. *)
|
|
module Acquisitions = struct
|
|
include PrettyPrintable.MakePPSet (Acquisition)
|
|
|
|
(* use the fact that location/procname are ignored in comparisons *)
|
|
let lock_is_held lock acquisitions = mem (Acquisition.make_dummy lock) acquisitions
|
|
end
|
|
|
|
module LockState : sig
|
|
include AbstractDomain.WithTop
|
|
|
|
val acquire : procname:Typ.Procname.t -> loc:Location.t -> Lock.t -> t -> t
|
|
|
|
val release : Lock.t -> t -> t
|
|
|
|
val is_lock_taken : Event.t -> t -> bool
|
|
|
|
val get_acquisitions : t -> Acquisitions.t
|
|
end = struct
|
|
(* abstraction limit for lock counts *)
|
|
let max_lock_depth_allowed = 5
|
|
|
|
module LockCount = AbstractDomain.DownwardIntDomain (struct
|
|
let max = max_lock_depth_allowed
|
|
end)
|
|
|
|
module Map = AbstractDomain.InvertedMap (Lock) (LockCount)
|
|
|
|
(* [acquisitions] has the currently held locks, so as to avoid a linear fold in [get_acquisitions].
|
|
This should also increase sharing across returned values from [get_acquisitions]. *)
|
|
type t = {map: Map.t; acquisitions: Acquisitions.t}
|
|
|
|
let get_acquisitions {acquisitions} = acquisitions
|
|
|
|
let pp fmt {map; acquisitions} =
|
|
F.fprintf fmt "{map= %a; acquisitions= %a}" Map.pp map Acquisitions.pp acquisitions
|
|
|
|
|
|
let join lhs rhs =
|
|
let map = Map.join lhs.map rhs.map in
|
|
let acquisitions = Acquisitions.inter lhs.acquisitions rhs.acquisitions in
|
|
{map; acquisitions}
|
|
|
|
|
|
let widen ~prev ~next ~num_iters =
|
|
let map = Map.widen ~prev:prev.map ~next:next.map ~num_iters in
|
|
let acquisitions = Acquisitions.inter prev.acquisitions next.acquisitions in
|
|
{map; acquisitions}
|
|
|
|
|
|
let ( <= ) ~lhs ~rhs = Map.( <= ) ~lhs:lhs.map ~rhs:rhs.map
|
|
|
|
let top = {map= Map.top; acquisitions= Acquisitions.empty}
|
|
|
|
let is_top {map} = Map.is_top map
|
|
|
|
let is_lock_taken event {acquisitions} =
|
|
match event with
|
|
| Event.LockAcquire lock ->
|
|
Acquisitions.mem (Acquisition.make_dummy lock) acquisitions
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let acquire ~procname ~loc lock {map; acquisitions} =
|
|
let should_add_acquisition = ref false in
|
|
let map =
|
|
Map.update lock
|
|
(function
|
|
| None ->
|
|
(* lock was not already held, so add it to [acquisitions] *)
|
|
should_add_acquisition := true ;
|
|
Some LockCount.(increment top)
|
|
| Some count ->
|
|
Some (LockCount.increment count) )
|
|
map
|
|
in
|
|
let acquisitions =
|
|
if !should_add_acquisition then
|
|
let acquisition = Acquisition.make ~procname ~loc lock in
|
|
Acquisitions.add acquisition acquisitions
|
|
else acquisitions
|
|
in
|
|
{map; acquisitions}
|
|
|
|
|
|
let release lock {map; acquisitions} =
|
|
let should_remove_acquisition = ref false in
|
|
let map =
|
|
Map.update lock
|
|
(function
|
|
| None ->
|
|
None
|
|
| Some count ->
|
|
let new_count = LockCount.decrement count in
|
|
if LockCount.is_top new_count then (
|
|
(* lock was held, but now it is not, so remove from [aqcuisitions] *)
|
|
should_remove_acquisition := true ;
|
|
None )
|
|
else Some new_count )
|
|
map
|
|
in
|
|
let acquisitions =
|
|
if !should_remove_acquisition then
|
|
let acquisition = Acquisition.make_dummy lock in
|
|
Acquisitions.remove acquisition acquisitions
|
|
else acquisitions
|
|
in
|
|
{map; acquisitions}
|
|
end
|
|
|
|
module CriticalPairElement = struct
|
|
type t = {acquisitions: Acquisitions.t; event: Event.t; thread: ThreadDomain.t}
|
|
[@@deriving compare]
|
|
|
|
let pp fmt {acquisitions; event} =
|
|
F.fprintf fmt "{acquisitions= %a; event= %a}" Acquisitions.pp acquisitions Event.pp event
|
|
|
|
|
|
let describe = pp
|
|
end
|
|
|
|
module CriticalPair = struct
|
|
include ExplicitTrace.MakeTraceElem (CriticalPairElement) (ExplicitTrace.DefaultCallPrinter)
|
|
|
|
let make ~loc acquisitions event thread = make {acquisitions; event; thread} loc
|
|
|
|
let is_blocking_call {elem= {event}} = match event with LockAcquire _ -> true | _ -> false
|
|
|
|
let get_final_acquire {elem= {event}} =
|
|
match event with LockAcquire lock -> Some lock | _ -> None
|
|
|
|
|
|
let may_deadlock ({elem= pair1} as t1 : t) ({elem= pair2} as t2 : t) =
|
|
ThreadDomain.can_run_in_parallel pair1.thread pair2.thread
|
|
&& Option.both (get_final_acquire t1) (get_final_acquire t2)
|
|
|> Option.exists ~f:(fun (lock1, lock2) ->
|
|
(not (Lock.equal lock1 lock2))
|
|
&& Acquisitions.lock_is_held lock2 pair1.acquisitions
|
|
&& Acquisitions.lock_is_held lock1 pair2.acquisitions
|
|
&& Acquisitions.inter pair1.acquisitions pair2.acquisitions |> Acquisitions.is_empty
|
|
)
|
|
|
|
|
|
let with_callsite t existing_acquisitions call_site thread =
|
|
let f (elem : CriticalPairElement.t) =
|
|
{ elem with
|
|
acquisitions= Acquisitions.union existing_acquisitions elem.acquisitions
|
|
; thread= ThreadDomain.integrate_summary ~caller:thread ~callee:elem.thread }
|
|
in
|
|
let new_t = map ~f t in
|
|
with_callsite new_t call_site
|
|
|
|
|
|
let get_earliest_lock_or_call_loc ~procname ({elem= {acquisitions}} as t) =
|
|
let initial_loc = get_loc t in
|
|
Acquisitions.fold
|
|
(fun {procname= acq_procname; loc= acq_loc} acc ->
|
|
if
|
|
Typ.Procname.equal procname acq_procname
|
|
&& Int.is_negative (Location.compare acq_loc acc)
|
|
then acq_loc
|
|
else acc )
|
|
acquisitions initial_loc
|
|
|
|
|
|
let make_trace ?(header = "") ?(include_acquisitions = true) top_pname
|
|
({elem= {acquisitions; event}; trace; loc} as pair) =
|
|
let acquisitions_map =
|
|
if include_acquisitions then
|
|
Acquisitions.fold
|
|
(fun ({procname} as acq : Acquisition.t) acc ->
|
|
Typ.Procname.Map.update procname
|
|
(function None -> Some [acq] | Some acqs -> Some (acq :: acqs))
|
|
acc )
|
|
acquisitions Typ.Procname.Map.empty
|
|
else Typ.Procname.Map.empty
|
|
in
|
|
let header_step =
|
|
let description = F.asprintf "%s%a" header pname_pp top_pname in
|
|
let loc = get_loc pair in
|
|
Errlog.make_trace_element 0 loc description []
|
|
in
|
|
(* construct the trace segment starting at [call_site] and ending at next call *)
|
|
let make_call_stack_step fake_first_call call_site =
|
|
let procname = CallSite.pname call_site in
|
|
let trace =
|
|
Typ.Procname.Map.find_opt procname acquisitions_map
|
|
|> Option.value ~default:[]
|
|
(* many acquisitions can be on same line (eg, std::lock) so use stable sort
|
|
to produce a deterministic trace *)
|
|
|> List.stable_sort ~compare:Acquisition.compare_loc
|
|
|> List.map ~f:Acquisition.make_trace_step
|
|
in
|
|
if CallSite.equal call_site fake_first_call then trace
|
|
else
|
|
let descr = F.asprintf "%a" ExplicitTrace.DefaultCallPrinter.pp call_site in
|
|
let call_step = Errlog.make_trace_element 0 (CallSite.loc call_site) descr [] in
|
|
call_step :: trace
|
|
in
|
|
(* construct a call stack trace with the lock acquisitions interleaved *)
|
|
let call_stack =
|
|
(* fake outermost call so as to include acquisitions in the top level caller *)
|
|
let fake_first_call = CallSite.make top_pname Location.dummy in
|
|
List.map (fake_first_call :: trace) ~f:(make_call_stack_step fake_first_call)
|
|
in
|
|
let endpoint_step =
|
|
let endpoint_descr = F.asprintf "%a" Event.describe event in
|
|
Errlog.make_trace_element 0 loc endpoint_descr []
|
|
in
|
|
List.concat (([header_step] :: call_stack) @ [[endpoint_step]])
|
|
|
|
|
|
let is_uithread t = ThreadDomain.is_uithread t.elem.thread
|
|
|
|
let can_run_in_parallel t1 t2 = ThreadDomain.can_run_in_parallel t1.elem.thread t2.elem.thread
|
|
end
|
|
|
|
let is_recursive_lock event tenv =
|
|
let is_class_and_recursive_lock = function
|
|
| {Typ.desc= Tptr ({desc= Tstruct name}, _)} | {desc= Tstruct name} ->
|
|
ConcurrencyModels.is_recursive_lock_type name
|
|
| typ ->
|
|
L.debug Analysis Verbose "Asked if non-struct type %a is a recursive lock type.@."
|
|
(Typ.pp_full Pp.text) typ ;
|
|
true
|
|
in
|
|
match event with
|
|
| Event.LockAcquire lock_path ->
|
|
AccessPath.get_typ lock_path tenv |> Option.exists ~f:is_class_and_recursive_lock
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** skip adding an order pair [(_, event)] if
|
|
- we have no tenv, or,
|
|
- [event] is not a lock event, or,
|
|
- we do not hold the lock, or,
|
|
- the lock is not recursive. *)
|
|
let should_skip tenv_opt event lock_state =
|
|
Option.exists tenv_opt ~f:(fun tenv ->
|
|
LockState.is_lock_taken event lock_state && is_recursive_lock event tenv )
|
|
|
|
|
|
module CriticalPairs = struct
|
|
include CriticalPair.FiniteSet
|
|
|
|
let with_callsite astate tenv lock_state call_site thread =
|
|
let existing_acquisitions = LockState.get_acquisitions lock_state in
|
|
fold
|
|
(fun ({elem= {event}} as critical_pair : CriticalPair.t) acc ->
|
|
if should_skip (Some tenv) event lock_state then acc
|
|
else
|
|
let new_pair =
|
|
CriticalPair.with_callsite critical_pair existing_acquisitions call_site thread
|
|
in
|
|
add new_pair acc )
|
|
astate empty
|
|
end
|
|
|
|
module FlatLock = AbstractDomain.Flat (Lock)
|
|
|
|
module GuardToLockMap = struct
|
|
include AbstractDomain.InvertedMap (HilExp) (FlatLock)
|
|
|
|
let remove_guard astate guard = remove guard astate
|
|
|
|
let add_guard astate ~guard ~lock = add guard (FlatLock.v lock) astate
|
|
end
|
|
|
|
type t =
|
|
{ guard_map: GuardToLockMap.t
|
|
; lock_state: LockState.t
|
|
; critical_pairs: CriticalPairs.t
|
|
; thread: ThreadDomain.t }
|
|
|
|
let bottom =
|
|
{ guard_map= GuardToLockMap.empty
|
|
; lock_state= LockState.top
|
|
; critical_pairs= CriticalPairs.empty
|
|
; thread= ThreadDomain.top }
|
|
|
|
|
|
let is_bottom {guard_map; lock_state; critical_pairs; thread} =
|
|
GuardToLockMap.is_empty guard_map && LockState.is_top lock_state
|
|
&& CriticalPairs.is_empty critical_pairs
|
|
&& ThreadDomain.is_top thread
|
|
|
|
|
|
let pp fmt {guard_map; lock_state; critical_pairs; thread} =
|
|
F.fprintf fmt "{guard_map= %a; lock_state= %a; critical_pairs= %a; thread= %a}" GuardToLockMap.pp
|
|
guard_map LockState.pp lock_state CriticalPairs.pp critical_pairs ThreadDomain.pp thread
|
|
|
|
|
|
let join lhs rhs =
|
|
{ guard_map= GuardToLockMap.join lhs.guard_map rhs.guard_map
|
|
; lock_state= LockState.join lhs.lock_state rhs.lock_state
|
|
; critical_pairs= CriticalPairs.join lhs.critical_pairs rhs.critical_pairs
|
|
; thread= ThreadDomain.join lhs.thread rhs.thread }
|
|
|
|
|
|
let widen ~prev ~next ~num_iters:_ = join prev next
|
|
|
|
let ( <= ) ~lhs ~rhs =
|
|
GuardToLockMap.( <= ) ~lhs:lhs.guard_map ~rhs:rhs.guard_map
|
|
&& LockState.( <= ) ~lhs:lhs.lock_state ~rhs:rhs.lock_state
|
|
&& CriticalPairs.( <= ) ~lhs:lhs.critical_pairs ~rhs:rhs.critical_pairs
|
|
&& ThreadDomain.( <= ) ~lhs:lhs.thread ~rhs:rhs.thread
|
|
|
|
|
|
let add_critical_pair tenv_opt lock_state event thread ~loc acc =
|
|
if should_skip tenv_opt event lock_state then acc
|
|
else
|
|
let acquisitions = LockState.get_acquisitions lock_state in
|
|
let critical_pair = CriticalPair.make ~loc acquisitions event thread in
|
|
CriticalPairs.add critical_pair acc
|
|
|
|
|
|
let acquire tenv ({lock_state; critical_pairs} as astate) ~procname ~loc locks =
|
|
{ astate with
|
|
critical_pairs=
|
|
List.fold locks ~init:critical_pairs ~f:(fun acc lock ->
|
|
let event = Event.make_acquire lock in
|
|
add_critical_pair (Some tenv) lock_state event astate.thread ~loc acc )
|
|
; lock_state=
|
|
List.fold locks ~init:lock_state ~f:(fun acc lock ->
|
|
LockState.acquire ~procname ~loc lock acc ) }
|
|
|
|
|
|
let make_call_with_event new_event ~loc astate =
|
|
{ astate with
|
|
critical_pairs=
|
|
add_critical_pair None astate.lock_state new_event astate.thread ~loc astate.critical_pairs
|
|
}
|
|
|
|
|
|
let blocking_call ~callee sev ~loc astate =
|
|
let new_event = Event.make_blocking_call callee sev in
|
|
make_call_with_event new_event ~loc astate
|
|
|
|
|
|
let strict_mode_call ~callee ~loc astate =
|
|
let new_event = Event.make_strict_mode_call callee in
|
|
make_call_with_event new_event ~loc astate
|
|
|
|
|
|
let release ({lock_state} as astate) locks =
|
|
{ astate with
|
|
lock_state= List.fold locks ~init:lock_state ~f:(fun acc l -> LockState.release l acc) }
|
|
|
|
|
|
let set_on_ui_thread astate = {astate with thread= ThreadDomain.UIThread}
|
|
|
|
let add_guard ~acquire_now ~procname ~loc tenv astate guard lock =
|
|
let astate = {astate with guard_map= GuardToLockMap.add_guard ~guard ~lock astate.guard_map} in
|
|
if acquire_now then acquire tenv astate ~procname ~loc [lock] else astate
|
|
|
|
|
|
let remove_guard astate guard =
|
|
GuardToLockMap.find_opt guard astate.guard_map
|
|
|> Option.value_map ~default:astate ~f:(fun lock_opt ->
|
|
let locks = FlatLock.get lock_opt |> Option.to_list in
|
|
let astate = release astate locks in
|
|
{astate with guard_map= GuardToLockMap.remove_guard astate.guard_map guard} )
|
|
|
|
|
|
let unlock_guard astate guard =
|
|
GuardToLockMap.find_opt guard astate.guard_map
|
|
|> Option.value_map ~default:astate ~f:(fun lock_opt ->
|
|
FlatLock.get lock_opt |> Option.to_list |> release astate )
|
|
|
|
|
|
let lock_guard ~procname ~loc tenv astate guard =
|
|
GuardToLockMap.find_opt guard astate.guard_map
|
|
|> Option.value_map ~default:astate ~f:(fun lock_opt ->
|
|
FlatLock.get lock_opt |> Option.to_list |> acquire tenv astate ~procname ~loc )
|
|
|
|
|
|
let filter_blocking_calls ({critical_pairs} as astate) =
|
|
{astate with critical_pairs= CriticalPairs.filter CriticalPair.is_blocking_call critical_pairs}
|
|
|
|
|
|
type summary = {critical_pairs: CriticalPairs.t; thread: ThreadDomain.t}
|
|
|
|
let pp_summary fmt (summary : summary) =
|
|
F.fprintf fmt "{thread= %a; critical_pairs= %a}" ThreadDomain.pp summary.thread CriticalPairs.pp
|
|
summary.critical_pairs
|
|
|
|
|
|
let integrate_summary tenv callsite (astate : t) (summary : summary) =
|
|
let critical_pairs' =
|
|
CriticalPairs.with_callsite summary.critical_pairs tenv astate.lock_state callsite
|
|
astate.thread
|
|
in
|
|
{ astate with
|
|
critical_pairs= CriticalPairs.join astate.critical_pairs critical_pairs'
|
|
; thread= ThreadDomain.integrate_summary ~caller:astate.thread ~callee:summary.thread }
|
|
|
|
|
|
let summary_of_astate : t -> summary =
|
|
fun astate -> {critical_pairs= astate.critical_pairs; thread= astate.thread}
|