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.
1161 lines
50 KiB
1161 lines
50 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 AccessExpression = HilExp.AccessExpression
|
|
module F = Format
|
|
module L = Logging
|
|
module MF = MarkupFormatter
|
|
|
|
let attrs_of_pname = Summary.OnDisk.proc_resolve_attributes
|
|
|
|
module Payload = SummaryPayload.Make (struct
|
|
type t = RacerDDomain.summary
|
|
|
|
let field = Payloads.Fields.racerd
|
|
end)
|
|
|
|
module TransferFunctions (CFG : ProcCfg.S) = struct
|
|
module CFG = CFG
|
|
module Domain = RacerDDomain
|
|
|
|
type extras = FormalMap.t
|
|
|
|
let add_access formals loc ~is_write_access locks threads ownership
|
|
(proc_data : extras ProcData.t) access_domain exp =
|
|
let open Domain in
|
|
let rec add_field_accesses prefix_path acc = function
|
|
| [] ->
|
|
acc
|
|
| access :: access_list ->
|
|
let prefix_path' = Option.value_exn (AccessExpression.add_access prefix_path access) in
|
|
if
|
|
(not (HilExp.Access.is_field_or_array_access access))
|
|
|| RacerDModels.is_safe_access access prefix_path proc_data.tenv
|
|
then add_field_accesses prefix_path' acc access_list
|
|
else
|
|
let is_write = List.is_empty access_list && is_write_access in
|
|
let access = TraceElem.make_field_access prefix_path' ~is_write loc in
|
|
let pre = OwnershipDomain.get_precondition prefix_path ownership in
|
|
let snapshot_opt = AccessSnapshot.make formals access locks threads pre in
|
|
let access_acc' = AccessDomain.add_opt snapshot_opt acc in
|
|
add_field_accesses prefix_path' access_acc' access_list
|
|
in
|
|
List.fold (HilExp.get_access_exprs exp) ~init:access_domain ~f:(fun acc access_expr ->
|
|
let base, accesses = AccessExpression.to_accesses access_expr in
|
|
add_field_accesses base acc accesses )
|
|
|
|
|
|
let make_container_access formals ret_base callee_pname ~is_write receiver_ap callee_loc tenv
|
|
(astate : Domain.t) =
|
|
let open Domain in
|
|
if RacerDModels.is_synchronized_container callee_pname receiver_ap tenv then None
|
|
else
|
|
let callee_access =
|
|
let container_access =
|
|
TraceElem.make_container_access receiver_ap ~is_write callee_pname callee_loc
|
|
in
|
|
let ownership_pre = OwnershipDomain.get_precondition receiver_ap astate.ownership in
|
|
AccessSnapshot.make formals container_access astate.locks astate.threads ownership_pre
|
|
in
|
|
let ownership_value = OwnershipDomain.get_owned receiver_ap astate.ownership in
|
|
let ownership =
|
|
OwnershipDomain.add (AccessExpression.base ret_base) ownership_value astate.ownership
|
|
in
|
|
let accesses = AccessDomain.add_opt callee_access astate.accesses in
|
|
Some {astate with accesses; ownership}
|
|
|
|
|
|
let add_reads formals exps loc ({accesses; locks; threads; ownership} as astate : Domain.t)
|
|
proc_data =
|
|
let accesses' =
|
|
List.fold exps ~init:accesses
|
|
~f:(add_access formals loc ~is_write_access:false locks threads ownership proc_data)
|
|
in
|
|
{astate with accesses= accesses'}
|
|
|
|
|
|
let expand_actuals formals actuals accesses pdesc =
|
|
let open Domain in
|
|
if AccessDomain.is_empty accesses then accesses
|
|
else
|
|
let rec get_access_exp = function
|
|
| HilExp.AccessExpression access_expr ->
|
|
Some access_expr
|
|
| HilExp.Cast (_, e) | HilExp.Exception e ->
|
|
get_access_exp e
|
|
| _ ->
|
|
None
|
|
in
|
|
let formal_map = FormalMap.make pdesc in
|
|
let expand_exp exp =
|
|
match FormalMap.get_formal_index (AccessExpression.get_base exp) formal_map with
|
|
| Some formal_index -> (
|
|
match List.nth actuals formal_index with
|
|
| Some actual_exp -> (
|
|
match get_access_exp actual_exp with
|
|
| Some actual ->
|
|
AccessExpression.append ~onto:actual exp |> Option.value ~default:exp
|
|
| None ->
|
|
exp )
|
|
| None ->
|
|
exp )
|
|
| None ->
|
|
exp
|
|
in
|
|
let add snapshot acc =
|
|
let access' = TraceElem.map ~f:expand_exp snapshot.AccessSnapshot.access in
|
|
let snapshot_opt' = AccessSnapshot.make_from_snapshot formals access' snapshot in
|
|
AccessDomain.add_opt snapshot_opt' acc
|
|
in
|
|
AccessDomain.fold add accesses AccessDomain.empty
|
|
|
|
|
|
let add_callee_accesses formals (caller_astate : Domain.t) callee_accesses locks threads actuals
|
|
callee_pname loc =
|
|
let open Domain in
|
|
let conjoin_ownership_precondition actual_indexes actual_exp :
|
|
AccessSnapshot.OwnershipPrecondition.t =
|
|
match actual_exp with
|
|
| HilExp.Constant _ ->
|
|
(* the actual is a constant, so it's owned in the caller. *)
|
|
Conjunction actual_indexes
|
|
| HilExp.AccessExpression access_expr -> (
|
|
match OwnershipDomain.get_owned access_expr caller_astate.ownership with
|
|
| OwnedIf formal_indexes ->
|
|
(* conditionally owned if [formal_indexes] are owned *)
|
|
Conjunction (IntSet.union formal_indexes actual_indexes)
|
|
| Unowned ->
|
|
(* not rooted in a formal and not conditionally owned *)
|
|
False )
|
|
| _ ->
|
|
(* couldn't find access expr, don't know if it's owned. assume not *)
|
|
False
|
|
in
|
|
let update_ownership_precondition actual_index (acc : AccessSnapshot.OwnershipPrecondition.t) =
|
|
match acc with
|
|
| False ->
|
|
(* precondition can't be satisfied *)
|
|
acc
|
|
| Conjunction actual_indexes ->
|
|
List.nth actuals actual_index
|
|
(* optional args can result into missing actuals so simply ignore *)
|
|
|> Option.value_map ~default:acc ~f:(conjoin_ownership_precondition actual_indexes)
|
|
in
|
|
let update_callee_access (snapshot : AccessSnapshot.t) acc =
|
|
let access = TraceElem.with_callsite snapshot.access (CallSite.make callee_pname loc) in
|
|
let locks = if snapshot.lock then LocksDomain.acquire_lock locks else locks in
|
|
let thread =
|
|
ThreadsDomain.integrate_summary ~callee_astate:snapshot.thread ~caller_astate:threads
|
|
in
|
|
(* update precondition with caller ownership info *)
|
|
let ownership_precondition =
|
|
match snapshot.ownership_precondition with
|
|
| Conjunction indexes ->
|
|
let empty_precondition =
|
|
AccessSnapshot.OwnershipPrecondition.Conjunction IntSet.empty
|
|
in
|
|
IntSet.fold update_ownership_precondition indexes empty_precondition
|
|
| False ->
|
|
snapshot.ownership_precondition
|
|
in
|
|
if AccessSnapshot.OwnershipPrecondition.is_true ownership_precondition then
|
|
(* discard accesses to owned memory *)
|
|
acc
|
|
else
|
|
let snapshot_opt = AccessSnapshot.make formals access locks thread ownership_precondition in
|
|
AccessDomain.add_opt snapshot_opt acc
|
|
in
|
|
AccessDomain.fold update_callee_access callee_accesses caller_astate.accesses
|
|
|
|
|
|
let call_without_summary callee_pname ret_base call_flags actuals astate =
|
|
let open RacerDModels in
|
|
let open RacerDDomain in
|
|
let should_assume_returns_ownership callee_pname (call_flags : CallFlags.t) actuals =
|
|
Config.racerd_unknown_returns_owned
|
|
(* non-interface methods with no summary and no parameters *)
|
|
|| ((not call_flags.cf_interface) && List.is_empty actuals)
|
|
|| (* static [$Builder] creation methods *)
|
|
creates_builder callee_pname
|
|
in
|
|
let should_assume_returns_conditional_ownership callee_pname =
|
|
(* non-interface methods with no parameters *)
|
|
is_abstract_getthis_like callee_pname
|
|
|| (* non-static [$Builder] methods with same return type as receiver type *)
|
|
is_builder_passthrough callee_pname
|
|
in
|
|
if is_box callee_pname then
|
|
match actuals with
|
|
| HilExp.AccessExpression actual_access_expr :: _ ->
|
|
if AttributeMapDomain.has_attribute actual_access_expr Functional astate.attribute_map
|
|
then
|
|
(* TODO: check for constants, which are functional? *)
|
|
let attribute_map =
|
|
AttributeMapDomain.add (AccessExpression.base ret_base) Functional
|
|
astate.attribute_map
|
|
in
|
|
{astate with attribute_map}
|
|
else astate
|
|
| _ ->
|
|
astate
|
|
else if should_assume_returns_ownership callee_pname call_flags actuals then
|
|
let ownership =
|
|
OwnershipDomain.add (AccessExpression.base ret_base) OwnershipAbstractValue.owned
|
|
astate.ownership
|
|
in
|
|
{astate with ownership}
|
|
else if should_assume_returns_conditional_ownership callee_pname then
|
|
(* assume abstract, single-parameter methods whose return type is equal to that of the first
|
|
formal return conditional ownership -- an example is getThis in Litho *)
|
|
let ownership =
|
|
OwnershipDomain.add (AccessExpression.base ret_base)
|
|
(OwnershipAbstractValue.make_owned_if 0)
|
|
astate.ownership
|
|
in
|
|
{astate with ownership}
|
|
else astate
|
|
|
|
|
|
let treat_call_acquiring_ownership ret_base procname actuals loc
|
|
({ProcData.tenv; extras} as proc_data) astate () =
|
|
let open Domain in
|
|
if RacerDModels.acquires_ownership procname tenv then
|
|
let astate = add_reads extras actuals loc astate proc_data in
|
|
let ownership =
|
|
OwnershipDomain.add (AccessExpression.base ret_base) OwnershipAbstractValue.owned
|
|
astate.ownership
|
|
in
|
|
Some {astate with ownership}
|
|
else None
|
|
|
|
|
|
let treat_container_accesses ret_base callee_pname actuals loc {ProcData.tenv; extras} astate () =
|
|
let open RacerDModels in
|
|
Option.bind (get_container_access callee_pname tenv) ~f:(fun container_access ->
|
|
match List.hd actuals with
|
|
| Some (HilExp.AccessExpression receiver_expr) ->
|
|
let is_write =
|
|
match container_access with ContainerWrite -> true | ContainerRead -> false
|
|
in
|
|
make_container_access extras ret_base callee_pname ~is_write receiver_expr loc tenv
|
|
astate
|
|
| _ ->
|
|
L.internal_error "Call to %a is marked as a container write, but has no receiver"
|
|
Procname.pp callee_pname ;
|
|
None )
|
|
|
|
|
|
let do_proc_call ret_base callee_pname actuals call_flags loc {ProcData.tenv; summary; extras}
|
|
(astate : Domain.t) () =
|
|
let open Domain in
|
|
let open RacerDModels in
|
|
let open ConcurrencyModels in
|
|
let ret_access_exp = AccessExpression.base ret_base in
|
|
let astate =
|
|
if RacerDModels.should_flag_interface_call tenv actuals call_flags callee_pname then
|
|
Domain.add_unannotated_call_access extras callee_pname loc astate
|
|
else astate
|
|
in
|
|
let astate =
|
|
match get_thread_assert_effect callee_pname with
|
|
| BackgroundThread ->
|
|
{astate with threads= ThreadsDomain.AnyThread}
|
|
| MainThread ->
|
|
{astate with threads= ThreadsDomain.AnyThreadButSelf}
|
|
| MainThreadIfTrue ->
|
|
let attribute_map =
|
|
AttributeMapDomain.add ret_access_exp Attribute.OnMainThread astate.attribute_map
|
|
in
|
|
{astate with attribute_map}
|
|
| UnknownThread ->
|
|
astate
|
|
in
|
|
let astate_callee =
|
|
(* assuming that modeled procedures do not have useful summaries *)
|
|
if is_thread_utils_method "assertMainThread" callee_pname then
|
|
{astate with threads= ThreadsDomain.AnyThreadButSelf}
|
|
else
|
|
(* if we don't have any evidence about whether the current function can run in parallel
|
|
with other threads or not, start assuming that it can. why use a lock if the function
|
|
can't run in a multithreaded context? *)
|
|
let update_for_lock_use = function
|
|
| ThreadsDomain.AnyThreadButSelf ->
|
|
ThreadsDomain.AnyThreadButSelf
|
|
| _ ->
|
|
ThreadsDomain.AnyThread
|
|
in
|
|
match get_lock_effect callee_pname actuals with
|
|
| Lock _ | GuardLock _ | GuardConstruct {acquire_now= true} ->
|
|
{ astate with
|
|
locks= LocksDomain.acquire_lock astate.locks
|
|
; threads= update_for_lock_use astate.threads }
|
|
| Unlock _ | GuardDestroy _ | GuardUnlock _ ->
|
|
{ astate with
|
|
locks= LocksDomain.release_lock astate.locks
|
|
; threads= update_for_lock_use astate.threads }
|
|
| LockedIfTrue _ | GuardLockedIfTrue _ ->
|
|
let attribute_map =
|
|
AttributeMapDomain.add ret_access_exp Attribute.LockHeld astate.attribute_map
|
|
in
|
|
{astate with attribute_map; threads= update_for_lock_use astate.threads}
|
|
| GuardConstruct {acquire_now= false} ->
|
|
astate
|
|
| NoEffect -> (
|
|
let rebased_summary_opt =
|
|
Payload.read ~caller_summary:summary ~callee_pname
|
|
|> Option.map ~f:(fun summary ->
|
|
let rebased_accesses =
|
|
Ondemand.get_proc_desc callee_pname
|
|
|> Option.fold ~init:summary.accesses ~f:(expand_actuals extras actuals)
|
|
in
|
|
{summary with accesses= rebased_accesses} )
|
|
in
|
|
match rebased_summary_opt with
|
|
| Some {threads; locks; accesses; return_ownership; return_attribute} ->
|
|
let locks =
|
|
LocksDomain.integrate_summary ~caller_astate:astate.locks ~callee_astate:locks
|
|
in
|
|
let accesses =
|
|
add_callee_accesses extras astate accesses locks threads actuals callee_pname loc
|
|
in
|
|
let ownership =
|
|
OwnershipDomain.propagate_return ret_access_exp return_ownership actuals
|
|
astate.ownership
|
|
in
|
|
let attribute_map =
|
|
AttributeMapDomain.add ret_access_exp return_attribute astate.attribute_map
|
|
in
|
|
let threads =
|
|
ThreadsDomain.integrate_summary ~caller_astate:astate.threads
|
|
~callee_astate:threads
|
|
in
|
|
{locks; threads; accesses; ownership; attribute_map}
|
|
| None ->
|
|
call_without_summary callee_pname ret_base call_flags actuals astate )
|
|
in
|
|
let add_if_annotated predicate attribute attribute_map =
|
|
if PatternMatch.override_exists predicate tenv callee_pname then
|
|
AttributeMapDomain.add ret_access_exp attribute attribute_map
|
|
else attribute_map
|
|
in
|
|
let attribute_map = add_if_annotated is_functional Functional astate_callee.attribute_map in
|
|
let ownership =
|
|
if
|
|
PatternMatch.override_exists
|
|
(has_return_annot Annotations.ia_is_returns_ownership)
|
|
tenv callee_pname
|
|
then OwnershipDomain.add ret_access_exp OwnershipAbstractValue.owned astate_callee.ownership
|
|
else astate_callee.ownership
|
|
in
|
|
{astate_callee with ownership; attribute_map}
|
|
|
|
|
|
let do_assignment lhs_access_exp rhs_exp loc ({ProcData.tenv; extras} as proc_data)
|
|
(astate : Domain.t) =
|
|
let open Domain in
|
|
let rhs_accesses =
|
|
add_access extras loc ~is_write_access:false astate.locks astate.threads astate.ownership
|
|
proc_data astate.accesses rhs_exp
|
|
in
|
|
let rhs_access_exprs = HilExp.get_access_exprs rhs_exp in
|
|
let is_functional =
|
|
(not (List.is_empty rhs_access_exprs))
|
|
&& List.for_all rhs_access_exprs ~f:(fun access_exp ->
|
|
AttributeMapDomain.has_attribute access_exp Functional astate.attribute_map )
|
|
&&
|
|
match AccessExpression.get_typ lhs_access_exp tenv with
|
|
| Some {Typ.desc= Typ.Tint ILong | Tfloat FDouble} ->
|
|
(* writes to longs and doubles are not guaranteed to be atomic in Java
|
|
(http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7), so there
|
|
can be a race even if the RHS is functional *)
|
|
false
|
|
| _ ->
|
|
true
|
|
in
|
|
let accesses =
|
|
if is_functional then
|
|
(* we want to forget about writes to @Functional fields altogether, otherwise we'll
|
|
report spurious read/write races *)
|
|
rhs_accesses
|
|
else
|
|
add_access extras loc ~is_write_access:true astate.locks astate.threads astate.ownership
|
|
proc_data rhs_accesses (HilExp.AccessExpression lhs_access_exp)
|
|
in
|
|
let ownership = OwnershipDomain.propagate_assignment lhs_access_exp rhs_exp astate.ownership in
|
|
let attribute_map =
|
|
AttributeMapDomain.propagate_assignment lhs_access_exp rhs_exp astate.attribute_map
|
|
in
|
|
{astate with accesses; ownership; attribute_map}
|
|
|
|
|
|
let do_assume formals assume_exp loc proc_data (astate : Domain.t) =
|
|
let open Domain in
|
|
let apply_choice bool_value (acc : Domain.t) = function
|
|
| Attribute.LockHeld ->
|
|
let locks =
|
|
if bool_value then LocksDomain.acquire_lock acc.locks
|
|
else LocksDomain.release_lock acc.locks
|
|
in
|
|
{acc with locks}
|
|
| Attribute.OnMainThread ->
|
|
let threads =
|
|
if bool_value then ThreadsDomain.AnyThreadButSelf else ThreadsDomain.AnyThread
|
|
in
|
|
{acc with threads}
|
|
| Attribute.(Functional | Nothing) ->
|
|
acc
|
|
in
|
|
let accesses =
|
|
add_access formals loc ~is_write_access:false astate.locks astate.threads astate.ownership
|
|
proc_data astate.accesses assume_exp
|
|
in
|
|
let astate' =
|
|
match HilExp.get_access_exprs assume_exp with
|
|
| [access_expr] ->
|
|
HilExp.eval_boolean_exp access_expr assume_exp
|
|
|> Option.value_map ~default:astate ~f:(fun bool_value ->
|
|
(* prune (prune_exp) can only evaluate to true if the choice is [bool_value].
|
|
add the constraint that the choice must be [bool_value] to the state *)
|
|
AttributeMapDomain.find access_expr astate.attribute_map
|
|
|> apply_choice bool_value astate )
|
|
| _ ->
|
|
astate
|
|
in
|
|
{astate' with accesses}
|
|
|
|
|
|
let exec_instr (astate : Domain.t) ({ProcData.summary; extras} as proc_data) _
|
|
(instr : HilInstr.t) =
|
|
match instr with
|
|
| Call (ret_base, Direct callee_pname, actuals, call_flags, loc) ->
|
|
let astate = add_reads extras actuals loc astate proc_data in
|
|
treat_call_acquiring_ownership ret_base callee_pname actuals loc proc_data astate ()
|
|
|> IOption.if_none_evalopt
|
|
~f:(treat_container_accesses ret_base callee_pname actuals loc proc_data astate)
|
|
|> IOption.if_none_eval
|
|
~f:(do_proc_call ret_base callee_pname actuals call_flags loc proc_data astate)
|
|
| Call (_, Indirect _, _, _, _) ->
|
|
if Procname.is_java (Summary.get_proc_name summary) then
|
|
L.(die InternalError) "Unexpected indirect call instruction %a" HilInstr.pp instr
|
|
else astate
|
|
| Assign (lhs_access_expr, rhs_exp, loc) ->
|
|
do_assignment lhs_access_expr rhs_exp loc proc_data astate
|
|
| Assume (assume_exp, _, _, loc) ->
|
|
do_assume extras assume_exp loc proc_data astate
|
|
| Metadata _ ->
|
|
astate
|
|
|
|
|
|
let pp_session_name _node fmt = F.pp_print_string fmt "racerd"
|
|
end
|
|
|
|
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (ProcCfg.Normal))
|
|
|
|
let analyze_procedure {Callbacks.exe_env; summary} =
|
|
let proc_desc = Summary.get_proc_desc summary in
|
|
let proc_name = Summary.get_proc_name summary in
|
|
let tenv = Exe_env.get_tenv exe_env proc_name in
|
|
let open RacerDModels in
|
|
let open ConcurrencyModels in
|
|
let method_annotation = (Procdesc.get_attributes proc_desc).method_annotation in
|
|
let is_initializer tenv proc_name =
|
|
Procname.is_constructor proc_name || FbThreadSafety.is_custom_init tenv proc_name
|
|
in
|
|
let open RacerDDomain in
|
|
if should_analyze_proc tenv proc_name then
|
|
let formal_map = FormalMap.make proc_desc in
|
|
let proc_data = ProcData.make summary tenv (FormalMap.make proc_desc) in
|
|
let initial =
|
|
let locks =
|
|
if Procdesc.is_java_synchronized proc_desc then LocksDomain.(acquire_lock bottom)
|
|
else LocksDomain.bottom
|
|
in
|
|
let threads =
|
|
if
|
|
runs_on_ui_thread ~attrs_of_pname tenv proc_name
|
|
|| is_thread_confined_method tenv proc_name
|
|
then ThreadsDomain.AnyThreadButSelf
|
|
else if Procdesc.is_java_synchronized proc_desc || is_marked_thread_safe proc_name tenv then
|
|
ThreadsDomain.AnyThread
|
|
else ThreadsDomain.NoThread
|
|
in
|
|
let add_owned_local acc (var_data : ProcAttributes.var_data) =
|
|
let pvar = Pvar.mk var_data.name proc_name in
|
|
let base = AccessPath.base_of_pvar pvar var_data.typ in
|
|
OwnershipDomain.add (AccessExpression.base base) OwnershipAbstractValue.owned acc
|
|
in
|
|
(* Add ownership to local variables. In cpp, stack-allocated local
|
|
variables cannot be raced on as every thread has its own stack.
|
|
More generally, we will never be confident that a race exists on a local/temp. *)
|
|
let own_locals =
|
|
List.fold ~f:add_owned_local (Procdesc.get_locals proc_desc) ~init:OwnershipDomain.empty
|
|
in
|
|
let is_owned_formal {Annot.class_name} =
|
|
(* @InjectProp allocates a fresh object to bind to the parameter *)
|
|
String.is_suffix ~suffix:Annotations.inject_prop class_name
|
|
in
|
|
let add_conditional_owned_formal acc (formal, formal_index) =
|
|
let ownership_value =
|
|
if Annotations.ma_has_annotation_with method_annotation is_owned_formal then
|
|
OwnershipAbstractValue.owned
|
|
else OwnershipAbstractValue.make_owned_if formal_index
|
|
in
|
|
OwnershipDomain.add (AccessExpression.base formal) ownership_value acc
|
|
in
|
|
if is_initializer tenv proc_name then
|
|
let add_owned_formal acc formal_index =
|
|
match FormalMap.get_formal_base formal_index formal_map with
|
|
| Some base ->
|
|
OwnershipDomain.add (AccessExpression.base base) OwnershipAbstractValue.owned acc
|
|
| None ->
|
|
acc
|
|
in
|
|
let ownership =
|
|
(* if a constructor is called via DI, all of its formals will be freshly allocated and
|
|
therefore owned. we assume that constructors annotated with @Inject will only be
|
|
called via DI or using fresh parameters. *)
|
|
if Annotations.pdesc_has_return_annot proc_desc Annotations.ia_is_inject then
|
|
List.mapi ~f:(fun i _ -> i) (Procdesc.get_formals proc_desc)
|
|
|> List.fold ~f:add_owned_formal ~init:own_locals
|
|
else
|
|
(* express that the constructor owns [this] *)
|
|
let init = add_owned_formal own_locals 0 in
|
|
FormalMap.get_formals_indexes formal_map
|
|
|> List.filter ~f:(fun (_, index) -> not (Int.equal 0 index))
|
|
|> List.fold ~init ~f:add_conditional_owned_formal
|
|
in
|
|
{RacerDDomain.bottom with ownership; threads; locks}
|
|
else
|
|
(* add Owned(formal_index) predicates for each formal to indicate that each one is owned if
|
|
it is owned in the caller *)
|
|
let ownership =
|
|
List.fold ~init:own_locals ~f:add_conditional_owned_formal
|
|
(FormalMap.get_formals_indexes formal_map)
|
|
in
|
|
{RacerDDomain.bottom with ownership; threads; locks}
|
|
in
|
|
match Analyzer.compute_post proc_data ~initial with
|
|
| Some {threads; locks; accesses; ownership; attribute_map} ->
|
|
let return_var_exp =
|
|
AccessExpression.base
|
|
(Var.of_pvar (Pvar.get_ret_pvar proc_name), Procdesc.get_ret_type proc_desc)
|
|
in
|
|
let return_ownership = OwnershipDomain.get_owned return_var_exp ownership in
|
|
let return_attribute = AttributeMapDomain.find return_var_exp attribute_map in
|
|
let locks =
|
|
(* if method is [synchronized] released the lock once. *)
|
|
if Procdesc.is_java_synchronized proc_desc then LocksDomain.release_lock locks else locks
|
|
in
|
|
let post = {threads; locks; accesses; return_ownership; return_attribute} in
|
|
Payload.update_summary post summary
|
|
| None ->
|
|
summary
|
|
else Payload.update_summary empty_summary summary
|
|
|
|
|
|
type conflict = RacerDDomain.TraceElem.t
|
|
|
|
type report_kind =
|
|
| GuardedByViolation
|
|
| WriteWriteRace of conflict option (** one of conflicting access, if there are any *)
|
|
| ReadWriteRace of conflict (** one of several conflicting accesses *)
|
|
| UnannotatedInterface
|
|
|
|
(** Explain why we are reporting this access, in Java *)
|
|
let get_reporting_explanation_java report_kind tenv pname thread =
|
|
let open RacerDModels in
|
|
(* best explanation is always that the current class or method is annotated thread-safe. try for
|
|
that first. *)
|
|
let annotation_explanation_opt =
|
|
if is_thread_safe_method pname tenv then
|
|
Some
|
|
(F.asprintf
|
|
"@\n Reporting because current method is annotated %a or overrides an annotated method."
|
|
MF.pp_monospaced "@ThreadSafe")
|
|
else
|
|
match FbThreadSafety.get_fbthreadsafe_class_annot pname tenv with
|
|
| Some (qual, annot) ->
|
|
Some (FbThreadSafety.message_fbthreadsafe_class qual annot)
|
|
| None -> (
|
|
match get_current_class_and_threadsafe_superclasses tenv pname with
|
|
| Some (current_class, (thread_safe_class :: _ as thread_safe_annotated_classes)) ->
|
|
Some
|
|
( if List.mem ~equal:Typ.Name.equal thread_safe_annotated_classes current_class then
|
|
F.asprintf "@\n Reporting because the current class is annotated %a"
|
|
MF.pp_monospaced "@ThreadSafe"
|
|
else
|
|
F.asprintf "@\n Reporting because a superclass %a is annotated %a"
|
|
(MF.wrap_monospaced Typ.Name.pp) thread_safe_class MF.pp_monospaced "@ThreadSafe"
|
|
)
|
|
| _ ->
|
|
None )
|
|
in
|
|
match (report_kind, annotation_explanation_opt) with
|
|
| GuardedByViolation, _ ->
|
|
( IssueType.guardedby_violation_racerd
|
|
, F.asprintf "@\n Reporting because field is annotated %a" MF.pp_monospaced "@GuardedBy" )
|
|
| UnannotatedInterface, Some threadsafe_explanation ->
|
|
(IssueType.interface_not_thread_safe, F.asprintf "%s." threadsafe_explanation)
|
|
| UnannotatedInterface, None ->
|
|
Logging.die InternalError
|
|
"Reporting non-threadsafe interface call, but can't find a @ThreadSafe annotation"
|
|
| _, Some threadsafe_explanation when RacerDDomain.ThreadsDomain.is_any thread ->
|
|
( IssueType.thread_safety_violation
|
|
, F.asprintf
|
|
"%s, so we assume that this method can run in parallel with other non-private methods in \
|
|
the class (including itself)."
|
|
threadsafe_explanation )
|
|
| _, Some threadsafe_explanation ->
|
|
( IssueType.thread_safety_violation
|
|
, F.asprintf
|
|
"%s. Although this access is not known to run on a background thread, it may happen in \
|
|
parallel with another access that does."
|
|
threadsafe_explanation )
|
|
| _, None ->
|
|
(* failed to explain based on @ThreadSafe annotation; have to justify using background thread *)
|
|
if RacerDDomain.ThreadsDomain.is_any thread then
|
|
( IssueType.thread_safety_violation
|
|
, F.asprintf "@\n Reporting because this access may occur on a background thread." )
|
|
else
|
|
( IssueType.thread_safety_violation
|
|
, F.asprintf
|
|
"@\n\
|
|
\ Reporting because another access to the same memory occurs on a background thread, \
|
|
although this access may not." )
|
|
|
|
|
|
(** Explain why we are reporting this access, in C++ *)
|
|
let get_reporting_explanation_cpp = (IssueType.lock_consistency_violation, "")
|
|
|
|
(** Explain why we are reporting this access *)
|
|
let get_reporting_explanation report_kind tenv pname thread =
|
|
if Procname.is_java pname then get_reporting_explanation_java report_kind tenv pname thread
|
|
else get_reporting_explanation_cpp
|
|
|
|
|
|
let describe_exp = MF.wrap_monospaced RacerDDomain.pp_exp
|
|
|
|
let describe_pname = MF.wrap_monospaced (Procname.pp_simplified_string ~withclass:true)
|
|
|
|
let pp_access fmt (t : RacerDDomain.TraceElem.t) =
|
|
match t.elem with
|
|
| Read {exp} | Write {exp} ->
|
|
describe_exp fmt exp
|
|
| ContainerRead {exp; pname} | ContainerWrite {exp; pname} ->
|
|
F.fprintf fmt "container %a via call to %a" describe_exp exp describe_pname pname
|
|
| InterfaceCall _ as access ->
|
|
RacerDDomain.Access.pp fmt access
|
|
|
|
|
|
let make_trace ~report_kind original_exp =
|
|
let open RacerDDomain in
|
|
let loc_trace_of_path path = TraceElem.make_loc_trace path in
|
|
let original_trace = loc_trace_of_path original_exp in
|
|
let get_end_loc trace = Option.map (List.last trace) ~f:(function {Errlog.lt_loc} -> lt_loc) in
|
|
let original_end = get_end_loc original_trace in
|
|
let make_with_conflicts conflict_sink original_trace ~label1 ~label2 =
|
|
(* create a trace for one of the conflicts and append it to the trace for the original sink *)
|
|
let conflict_trace = loc_trace_of_path conflict_sink in
|
|
let conflict_end = get_end_loc conflict_trace in
|
|
( Errlog.concat_traces [(label1, original_trace); (label2, conflict_trace)]
|
|
, original_end
|
|
, conflict_end )
|
|
in
|
|
match report_kind with
|
|
| ReadWriteRace conflict ->
|
|
make_with_conflicts conflict original_trace ~label1:"<Read trace>" ~label2:"<Write trace>"
|
|
| WriteWriteRace (Some conflict) ->
|
|
make_with_conflicts conflict original_trace ~label1:"<Write on unknown thread>"
|
|
~label2:"<Write on background thread>"
|
|
| GuardedByViolation | WriteWriteRace None | UnannotatedInterface ->
|
|
(original_trace, original_end, None)
|
|
|
|
|
|
let log_issue current_pname ~issue_log ~loc ~ltr ~access issue_type error_message =
|
|
Reporting.log_issue_external current_pname Exceptions.Warning ~issue_log ~loc ~ltr ~access
|
|
issue_type error_message
|
|
|
|
|
|
type reported_access =
|
|
{ threads: RacerDDomain.ThreadsDomain.t
|
|
; snapshot: RacerDDomain.AccessSnapshot.t
|
|
; tenv: Tenv.t
|
|
; procname: Procname.t }
|
|
|
|
let report_thread_safety_violation ~issue_log ~make_description ~report_kind
|
|
({threads; snapshot; tenv; procname= pname} : reported_access) =
|
|
let open RacerDDomain in
|
|
let access = snapshot.access in
|
|
let final_pname = List.last access.trace |> Option.value_map ~default:pname ~f:CallSite.pname in
|
|
let final_sink_site = CallSite.make final_pname access.loc in
|
|
let initial_sink_site = CallSite.make pname (TraceElem.get_loc access) in
|
|
let loc = CallSite.loc initial_sink_site in
|
|
let ltr, original_end, conflict_end = make_trace ~report_kind access in
|
|
(* what the potential bug is *)
|
|
let description = make_description pname final_sink_site initial_sink_site access in
|
|
(* why we are reporting it *)
|
|
let issue_type, explanation = get_reporting_explanation report_kind tenv pname threads in
|
|
let error_message = F.sprintf "%s%s" description explanation in
|
|
let end_locs = Option.to_list original_end @ Option.to_list conflict_end in
|
|
let access = IssueAuxData.encode end_locs in
|
|
log_issue pname ~issue_log ~loc ~ltr ~access issue_type error_message
|
|
|
|
|
|
let report_unannotated_interface_violation ~issue_log reported_pname reported_access =
|
|
match reported_pname with
|
|
| Procname.Java java_pname ->
|
|
let class_name = Procname.Java.get_class_name java_pname in
|
|
let make_description _ _ _ _ =
|
|
F.asprintf
|
|
"Unprotected call to method %a of un-annotated interface %a. Consider annotating the \
|
|
class with %a, adding a lock, or using an interface that is known to be thread-safe."
|
|
describe_pname reported_pname MF.pp_monospaced class_name MF.pp_monospaced "@ThreadSafe"
|
|
in
|
|
report_thread_safety_violation ~issue_log ~make_description ~report_kind:UnannotatedInterface
|
|
reported_access
|
|
| _ ->
|
|
(* skip reporting on C++ *)
|
|
issue_log
|
|
|
|
|
|
let make_unprotected_write_description pname final_sink_site initial_sink_site final_sink =
|
|
Format.asprintf "Unprotected write. Non-private method %a%s %s %a outside of synchronization."
|
|
describe_pname pname
|
|
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
|
|
(if RacerDDomain.TraceElem.is_container_write final_sink then "mutates" else "writes to field")
|
|
pp_access final_sink
|
|
|
|
|
|
let make_guardedby_violation_description pname final_sink_site initial_sink_site final_sink =
|
|
Format.asprintf
|
|
"GuardedBy violation. Non-private method %a%s accesses %a outside of synchronization."
|
|
describe_pname pname
|
|
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
|
|
pp_access final_sink
|
|
|
|
|
|
let make_read_write_race_description ~read_is_sync (conflict : reported_access) pname
|
|
final_sink_site initial_sink_site final_sink =
|
|
let pp_conflict fmt {procname} =
|
|
F.pp_print_string fmt (Procname.to_simplified_string ~withclass:true procname)
|
|
in
|
|
let conflicts_description =
|
|
Format.asprintf "Potentially races with%s write in method %a"
|
|
(if read_is_sync then " unsynchronized" else "")
|
|
(MF.wrap_monospaced pp_conflict) conflict
|
|
in
|
|
Format.asprintf "Read/Write race. Non-private method %a%s reads%s from %a. %s." describe_pname
|
|
pname
|
|
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
|
|
(if read_is_sync then " with synchronization" else " without synchronization")
|
|
pp_access final_sink conflicts_description
|
|
|
|
|
|
(** type for remembering what we have already reported to avoid duplicates. our policy is to report
|
|
each kind of access (read/write) to the same field reachable from the same procedure only once.
|
|
in addition, if a call to a procedure (transitively) accesses multiple fields, we will only
|
|
report one of each kind of access *)
|
|
type reported =
|
|
{ reported_sites: CallSite.Set.t
|
|
; reported_writes: Procname.Set.t
|
|
; reported_reads: Procname.Set.t
|
|
; reported_unannotated_calls: Procname.Set.t }
|
|
|
|
let empty_reported =
|
|
let reported_sites = CallSite.Set.empty in
|
|
let reported_writes = Procname.Set.empty in
|
|
let reported_reads = Procname.Set.empty in
|
|
let reported_unannotated_calls = Procname.Set.empty in
|
|
{reported_sites; reported_reads; reported_writes; reported_unannotated_calls}
|
|
|
|
|
|
(* decide if we should throw away an access before doing safety analysis
|
|
for now, just check for whether the access is within a switch-map
|
|
that is auto-generated by Java. *)
|
|
let should_filter_access exp_opt =
|
|
let check_access = function
|
|
| HilExp.Access.FieldAccess fld ->
|
|
String.is_substring ~substring:"$SwitchMap" (Fieldname.to_string fld)
|
|
| _ ->
|
|
false
|
|
in
|
|
Option.exists exp_opt ~f:(fun exp ->
|
|
AccessExpression.to_accesses exp |> snd |> List.exists ~f:check_access )
|
|
|
|
|
|
(** Map containing reported accesses, which groups them in lists, by abstract location. The
|
|
equivalence relation used for grouping them is equality of access paths. This is slightly
|
|
complicated because local variables contain the pname of the function declaring them. Here we
|
|
want a purely name-based comparison, and in particular that [this == this] regardless the method
|
|
declaring it. Hence the redefined comparison functions. *)
|
|
module ReportMap : sig
|
|
type t
|
|
|
|
val empty : t
|
|
|
|
val add : reported_access -> t -> t
|
|
|
|
val fold : (reported_access list -> 'a -> 'a) -> t -> 'a -> 'a
|
|
end = struct
|
|
module PathModuloThis : Caml.Map.OrderedType with type t = AccessPath.t = struct
|
|
type t = AccessPath.t
|
|
|
|
type var_ = Var.t
|
|
|
|
let compare_var_ = Var.compare_modulo_this
|
|
|
|
let compare = [%compare: (var_ * Typ.t) * AccessPath.access list]
|
|
end
|
|
|
|
module Key = struct
|
|
type t = Location of PathModuloThis.t | Container of PathModuloThis.t | Call of Procname.t
|
|
[@@deriving compare]
|
|
|
|
let of_access (access : RacerDDomain.Access.t) =
|
|
match access with
|
|
| Read {exp} | Write {exp} ->
|
|
Location (AccessExpression.to_access_path exp)
|
|
| ContainerRead {exp} | ContainerWrite {exp} ->
|
|
Container (AccessExpression.to_access_path exp)
|
|
| InterfaceCall pn ->
|
|
Call pn
|
|
end
|
|
|
|
module M = Caml.Map.Make (Key)
|
|
|
|
type t = reported_access list M.t
|
|
|
|
let empty = M.empty
|
|
|
|
let add (rep : reported_access) map =
|
|
let access = rep.snapshot.access.elem in
|
|
if RacerDDomain.Access.get_access_exp access |> should_filter_access then map
|
|
else
|
|
let k = Key.of_access access in
|
|
M.update k (function None -> Some [rep] | Some reps -> Some (rep :: reps)) map
|
|
|
|
|
|
let fold f map a =
|
|
let f _ v acc = f v acc in
|
|
M.fold f map a
|
|
end
|
|
|
|
let should_report_on_proc tenv procdesc =
|
|
let proc_name = Procdesc.get_proc_name procdesc in
|
|
match proc_name with
|
|
| Java java_pname ->
|
|
(* return true if procedure is at an abstraction boundary or reporting has been explicitly
|
|
requested via @ThreadSafe in java *)
|
|
RacerDModels.is_thread_safe_method proc_name tenv
|
|
|| (not (PredSymb.equal_access (Procdesc.get_access procdesc) Private))
|
|
&& (not (Procname.Java.is_autogen_method java_pname))
|
|
&& not (Annotations.pdesc_return_annot_ends_with procdesc Annotations.visibleForTesting)
|
|
| ObjC_Cpp {kind= CPPMethod _ | CPPConstructor _ | CPPDestructor _} ->
|
|
not (PredSymb.equal_access (Procdesc.get_access procdesc) Private)
|
|
| ObjC_Cpp {kind= ObjCClassMethod | ObjCInstanceMethod | ObjCInternalMethod; class_name} ->
|
|
Tenv.lookup tenv class_name
|
|
|> Option.exists ~f:(fun {Struct.exported_objc_methods} ->
|
|
List.mem ~equal:Procname.equal exported_objc_methods proc_name )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let should_report_guardedby_violation classname ({snapshot; tenv; procname} : reported_access) =
|
|
let is_uitthread param =
|
|
match String.lowercase param with
|
|
| "ui thread" | "ui-thread" | "ui_thread" | "uithread" ->
|
|
true
|
|
| _ ->
|
|
false
|
|
in
|
|
let field_is_annotated_guardedby field_name (f, _, a) =
|
|
Fieldname.equal f field_name
|
|
&& List.exists a ~f:(fun ((annot : Annot.t), _) ->
|
|
Annotations.annot_ends_with annot Annotations.guarded_by
|
|
&&
|
|
match annot.parameters with
|
|
| [param] ->
|
|
not (Annot.has_matching_str_value ~pred:is_uitthread param.value)
|
|
| _ ->
|
|
false )
|
|
in
|
|
(not snapshot.lock)
|
|
&& RacerDDomain.TraceElem.is_write snapshot.access
|
|
&& Procname.is_java procname
|
|
&&
|
|
(* restrict check to access paths of length one *)
|
|
match
|
|
RacerDDomain.Access.get_access_exp snapshot.access.elem
|
|
|> Option.map ~f:AccessExpression.to_accesses
|
|
|> Option.map ~f:(fun (base, accesses) ->
|
|
(base, List.filter accesses ~f:HilExp.Access.is_field_or_array_access) )
|
|
with
|
|
| Some (AccessExpression.Base (_, base_type), [HilExp.Access.FieldAccess field_name]) -> (
|
|
match base_type.desc with
|
|
| Tstruct base_name | Tptr ({desc= Tstruct base_name}, _) ->
|
|
(* is the base class a subclass of the one containing the GuardedBy annotation? *)
|
|
PatternMatch.is_subtype tenv base_name classname
|
|
&& Tenv.lookup tenv base_name
|
|
|> Option.exists ~f:(fun ({fields; statics} : Struct.t) ->
|
|
let f fld = field_is_annotated_guardedby field_name fld in
|
|
List.exists fields ~f || List.exists statics ~f )
|
|
| _ ->
|
|
false )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** Report accesses that may race with each other.
|
|
|
|
Principles for race reporting.
|
|
|
|
Two accesses are excluded if they are both protected by the same lock or are known to be on the
|
|
same thread. Otherwise they are in conflict. We want to report conflicting accesses one of which
|
|
is a write.
|
|
|
|
To cut down on duplication noise we don't always report at both sites (line numbers) involved in
|
|
a race.
|
|
|
|
\-- If a protected access races with an unprotected one, we don't report the protected but we do
|
|
report the unprotected one (and we point to the protected from the unprotected one). This way
|
|
the report is at the line number in a race-pair where the programmer should take action.
|
|
|
|
\-- Similarly, if a threaded and unthreaded (not known to be threaded) access race, we report at
|
|
the unthreaded site.
|
|
|
|
Also, we avoid reporting multiple races at the same line (which can happen a lot in an
|
|
interprocedural scenario) or multiple accesses to the same field in a single method, expecting
|
|
that the programmer already gets signal from one report. To report all the races with separate
|
|
warnings leads to a lot of noise. But note, we never suppress all the potential issues in a
|
|
class: if we don't report any races, it means we didn't find any.
|
|
|
|
The above is tempered at the moment by abstractions of "same lock" and "same thread": we are
|
|
currently not distinguishing different locks, and are treating "known to be confined to a
|
|
thread" as if "known to be confined to UI thread". *)
|
|
let report_unsafe_accesses ~issue_log classname (aggregated_access_map : ReportMap.t) =
|
|
let open RacerDDomain in
|
|
let open RacerDModels in
|
|
let is_duplicate_report ({snapshot; procname= pname} : reported_access)
|
|
({reported_sites; reported_writes; reported_reads; reported_unannotated_calls}, _) =
|
|
let call_site = CallSite.make pname (TraceElem.get_loc snapshot.access) in
|
|
if Config.deduplicate then
|
|
CallSite.Set.mem call_site reported_sites
|
|
||
|
|
match snapshot.access.TraceElem.elem with
|
|
| Access.Write _ | Access.ContainerWrite _ ->
|
|
Procname.Set.mem pname reported_writes
|
|
| Access.Read _ | Access.ContainerRead _ ->
|
|
Procname.Set.mem pname reported_reads
|
|
| Access.InterfaceCall _ ->
|
|
Procname.Set.mem pname reported_unannotated_calls
|
|
else false
|
|
in
|
|
let update_reported ({snapshot; procname= pname} : reported_access) reported =
|
|
if Config.deduplicate then
|
|
let call_site = CallSite.make pname (TraceElem.get_loc snapshot.access) in
|
|
let reported_sites = CallSite.Set.add call_site reported.reported_sites in
|
|
match snapshot.access.TraceElem.elem with
|
|
| Access.Write _ | Access.ContainerWrite _ ->
|
|
let reported_writes = Procname.Set.add pname reported.reported_writes in
|
|
{reported with reported_writes; reported_sites}
|
|
| Access.Read _ | Access.ContainerRead _ ->
|
|
let reported_reads = Procname.Set.add pname reported.reported_reads in
|
|
{reported with reported_reads; reported_sites}
|
|
| Access.InterfaceCall _ ->
|
|
let reported_unannotated_calls =
|
|
Procname.Set.add pname reported.reported_unannotated_calls
|
|
in
|
|
{reported with reported_unannotated_calls; reported_sites}
|
|
else reported
|
|
in
|
|
let report_thread_safety_violation ~acc ~make_description ~report_kind reported_access =
|
|
if is_duplicate_report reported_access acc then acc
|
|
else
|
|
let reported_acc, issue_log = acc in
|
|
let issue_log =
|
|
report_thread_safety_violation ~issue_log ~make_description ~report_kind reported_access
|
|
in
|
|
(update_reported reported_access reported_acc, issue_log)
|
|
in
|
|
let report_unannotated_interface_violation ~acc reported_pname reported_access =
|
|
if is_duplicate_report reported_access acc then acc
|
|
else
|
|
let reported_acc, issue_log = acc in
|
|
let issue_log =
|
|
report_unannotated_interface_violation ~issue_log reported_pname reported_access
|
|
in
|
|
(update_reported reported_access reported_acc, issue_log)
|
|
in
|
|
let report_unsafe_access accesses acc
|
|
({snapshot; threads; tenv; procname= pname} as reported_access) =
|
|
match snapshot.access.elem with
|
|
| Access.InterfaceCall reported_pname
|
|
when AccessSnapshot.is_unprotected snapshot
|
|
&& ThreadsDomain.is_any threads && is_marked_thread_safe pname tenv ->
|
|
(* un-annotated interface call + no lock in method marked thread-safe. warn *)
|
|
report_unannotated_interface_violation ~acc reported_pname reported_access
|
|
| Access.InterfaceCall _ ->
|
|
acc
|
|
| (Access.Write _ | ContainerWrite _) when Procname.is_java pname ->
|
|
let conflict =
|
|
if ThreadsDomain.is_any threads then
|
|
(* unprotected write in method that may run in parallel with itself. warn *)
|
|
None
|
|
else
|
|
(* unprotected write, but not on a method that may run in parallel with itself
|
|
(i.e., not a self race). find accesses on a background thread this access might
|
|
conflict with and report them *)
|
|
List.find_map accesses ~f:(fun {snapshot= other_snapshot; threads= other_threads} ->
|
|
if TraceElem.is_write other_snapshot.access && ThreadsDomain.is_any other_threads
|
|
then Some other_snapshot.access
|
|
else None )
|
|
in
|
|
if
|
|
AccessSnapshot.is_unprotected snapshot
|
|
&& (Option.is_some conflict || ThreadsDomain.is_any threads)
|
|
then
|
|
report_thread_safety_violation ~acc ~make_description:make_unprotected_write_description
|
|
~report_kind:(WriteWriteRace conflict) reported_access
|
|
else acc
|
|
| Access.Write _ | ContainerWrite _ ->
|
|
(* Do not report unprotected writes for ObjC_Cpp *)
|
|
acc
|
|
| (Access.Read _ | ContainerRead _) when AccessSnapshot.is_unprotected snapshot ->
|
|
(* unprotected read. report all writes as conflicts for java. for c++ filter out
|
|
unprotected writes *)
|
|
let is_conflict {snapshot; threads= other_threads} =
|
|
TraceElem.is_write snapshot.access
|
|
&&
|
|
if Procname.is_java pname then
|
|
ThreadsDomain.is_any threads || ThreadsDomain.is_any other_threads
|
|
else not (AccessSnapshot.is_unprotected snapshot)
|
|
in
|
|
List.find ~f:is_conflict accesses
|
|
|> Option.value_map ~default:acc ~f:(fun conflict ->
|
|
let make_description =
|
|
make_read_write_race_description ~read_is_sync:false conflict
|
|
in
|
|
let report_kind = ReadWriteRace conflict.snapshot.access in
|
|
report_thread_safety_violation ~acc ~make_description ~report_kind reported_access )
|
|
| Access.Read _ | ContainerRead _ ->
|
|
(* protected read. report unprotected writes and opposite protected writes as conflicts *)
|
|
let can_conflict (snapshot1 : AccessSnapshot.t) (snapshot2 : AccessSnapshot.t) =
|
|
if snapshot1.lock && snapshot2.lock then false
|
|
else ThreadsDomain.can_conflict snapshot1.thread snapshot2.thread
|
|
in
|
|
let is_conflict {snapshot= other_snapshot; threads= other_threads} =
|
|
if AccessSnapshot.is_unprotected other_snapshot then
|
|
TraceElem.is_write other_snapshot.access && ThreadsDomain.is_any other_threads
|
|
else TraceElem.is_write other_snapshot.access && can_conflict snapshot other_snapshot
|
|
in
|
|
List.find accesses ~f:is_conflict
|
|
|> Option.value_map ~default:acc ~f:(fun conflict ->
|
|
(* protected read with conflicting unprotected write(s). warn. *)
|
|
let make_description =
|
|
make_read_write_race_description ~read_is_sync:true conflict
|
|
in
|
|
let report_kind = ReadWriteRace conflict.snapshot.access in
|
|
report_thread_safety_violation ~acc ~make_description ~report_kind reported_access )
|
|
in
|
|
let report_accesses_on_location reportable_accesses init =
|
|
(* Don't report on location if all accesses are on non-concurrent contexts *)
|
|
if
|
|
List.for_all reportable_accesses ~f:(fun ({threads} : reported_access) ->
|
|
ThreadsDomain.is_any threads |> not )
|
|
then init
|
|
else List.fold reportable_accesses ~init ~f:(report_unsafe_access reportable_accesses)
|
|
in
|
|
let report_guardedby_violations_on_location grouped_accesses init =
|
|
if Config.racerd_guardedby then
|
|
List.fold grouped_accesses ~init ~f:(fun acc r ->
|
|
if should_report_guardedby_violation classname r then
|
|
report_thread_safety_violation ~acc ~report_kind:GuardedByViolation
|
|
~make_description:make_guardedby_violation_description r
|
|
else acc )
|
|
else init
|
|
in
|
|
let report grouped_accesses (reported, issue_log) =
|
|
(* reset the reported reads and writes for each memory location *)
|
|
let reported =
|
|
{reported with reported_writes= Procname.Set.empty; reported_reads= Procname.Set.empty}
|
|
in
|
|
report_guardedby_violations_on_location grouped_accesses (reported, issue_log)
|
|
|> report_accesses_on_location grouped_accesses
|
|
in
|
|
ReportMap.fold report aggregated_access_map (empty_reported, issue_log) |> snd
|
|
|
|
|
|
(* create a map from [abstraction of a memory loc] -> accesses that
|
|
may touch that memory loc. the abstraction of a location is an access
|
|
path like x.f.g whose concretization is the set of memory cells
|
|
that x.f.g may point to during execution *)
|
|
let make_results_table exe_env summaries =
|
|
let open RacerDDomain in
|
|
let aggregate_post tenv procname acc {threads; accesses} =
|
|
AccessDomain.fold
|
|
(fun snapshot acc -> ReportMap.add {threads; snapshot; tenv; procname} acc)
|
|
accesses acc
|
|
in
|
|
List.fold summaries ~init:ReportMap.empty ~f:(fun acc (summary : Summary.t) ->
|
|
let procname = Summary.get_proc_name summary in
|
|
let tenv = Exe_env.get_tenv exe_env procname in
|
|
Payloads.racerd summary.payloads |> Option.fold ~init:acc ~f:(aggregate_post tenv procname) )
|
|
|
|
|
|
let class_has_concurrent_method class_summaries =
|
|
let open RacerDDomain in
|
|
let method_has_concurrent_context (summary : Summary.t) =
|
|
Payloads.racerd summary.payloads
|
|
|> Option.exists ~f:(fun (payload : summary) ->
|
|
match (payload.threads : ThreadsDomain.t) with NoThread -> false | _ -> true )
|
|
in
|
|
List.exists class_summaries ~f:method_has_concurrent_context
|
|
|
|
|
|
let should_report_on_class (classname : Typ.Name.t) class_summaries =
|
|
match classname with
|
|
| JavaClass _ ->
|
|
true
|
|
| CppClass _ | ObjcClass _ | ObjcProtocol _ | CStruct _ ->
|
|
class_has_concurrent_method class_summaries
|
|
| CUnion _ ->
|
|
false
|
|
|
|
|
|
let filter_reportable_classes class_map = Typ.Name.Map.filter should_report_on_class class_map
|
|
|
|
(* aggregate all of the procedures in the file env by their declaring
|
|
class. this lets us analyze each class individually *)
|
|
let aggregate_by_class exe_env procedures =
|
|
List.fold procedures ~init:Typ.Name.Map.empty ~f:(fun acc procname ->
|
|
Procname.get_class_type_name procname
|
|
|> Option.bind ~f:(fun classname ->
|
|
Ondemand.analyze_proc_name_no_caller procname
|
|
|> Option.filter ~f:(fun summary ->
|
|
let pdesc = Summary.get_proc_desc summary in
|
|
let tenv = Exe_env.get_tenv exe_env procname in
|
|
should_report_on_proc tenv pdesc )
|
|
|> Option.map ~f:(fun summary ->
|
|
Typ.Name.Map.update classname
|
|
(function
|
|
| None -> Some [summary] | Some summaries -> Some (summary :: summaries) )
|
|
acc ) )
|
|
|> Option.value ~default:acc )
|
|
|> filter_reportable_classes
|
|
|
|
|
|
(* Gathers results by analyzing all the methods in a file, then
|
|
post-processes the results to check an (approximation of) thread
|
|
safety *)
|
|
let file_analysis ({procedures; exe_env} : Callbacks.file_callback_args) =
|
|
let class_map = aggregate_by_class exe_env procedures in
|
|
Typ.Name.Map.fold
|
|
(fun classname methods issue_log ->
|
|
make_results_table exe_env methods |> report_unsafe_accesses ~issue_log classname )
|
|
class_map IssueLog.empty
|