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.

986 lines
43 KiB

(*
* Copyright (c) 2016 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
open! IStd
module F = Format
module L = Logging
module Summary = Summary.Make (struct
type summary = ThreadSafetyDomain.summary
let update_payload summary payload =
{ payload with Specs.threadsafety = Some summary }
let read_from_payload payload =
payload.Specs.threadsafety
end)
let is_owned access_path attribute_map =
ThreadSafetyDomain.AttributeMapDomain.has_attribute
access_path ThreadSafetyDomain.Attribute.unconditionally_owned attribute_map
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ThreadSafetyDomain
type extras = FormalMap.t
type lock_model =
| Lock
| Unlock
| NoEffect
let get_lock_model = function
| Procname.Java java_pname ->
begin
match Procname.java_get_class_name java_pname, Procname.java_get_method java_pname with
| "java.util.concurrent.locks.Lock", "lock" ->
Lock
| ("java.util.concurrent.locks.ReentrantLock"
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock"),
("lock" | "tryLock" | "lockInterruptibly") ->
Lock
| ("java.util.concurrent.locks.Lock"
|"java.util.concurrent.locks.ReentrantLock"
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock"),
"unlock" ->
Unlock
| _ ->
NoEffect
end
| pname when Procname.equal pname BuiltinDecl.__set_locked_attribute ->
Lock
| pname when Procname.equal pname BuiltinDecl.__delete_locked_attribute ->
Unlock
| _ ->
NoEffect
let resolve_id (id_map : IdAccessPathMapDomain.astate) id =
try Some (IdAccessPathMapDomain.find id id_map)
with Not_found -> None
let is_constant = function
| Exp.Const _ -> true
| _ -> false
let add_conditional_ownership_attribute access_path formal_map attribute_map attributes =
match FormalMap.get_formal_index (fst access_path) formal_map with
| Some formal_index when not (is_owned access_path attribute_map) ->
Domain.AttributeSetDomain.add (Domain.Attribute.OwnedIf (Some formal_index)) attributes
| _ ->
attributes
(* if rhs has associated attributes, propagate them to the lhs *)
let propagate_attributes lhs_access_path rhs_exp rhs_typ ~f_resolve_id attribute_map formal_map =
let rhs_access_paths = AccessPath.of_exp rhs_exp rhs_typ ~f_resolve_id in
let rhs_attributes =
if List.is_empty rhs_access_paths (* only happens when rhs is a constant *)
then
(* rhs is a constant, and constants are both owned and functional *)
Domain.AttributeSetDomain.of_list
[Domain.Attribute.unconditionally_owned; Domain.Attribute.Functional]
else
let propagate_attributes_ acc rhs_access_path =
(try Domain.AttributeMapDomain.find rhs_access_path attribute_map
with Not_found -> acc)
|> add_conditional_ownership_attribute rhs_access_path formal_map attribute_map in
List.fold
~f:propagate_attributes_
~init:Domain.AttributeSetDomain.empty
rhs_access_paths in
Domain.AttributeMapDomain.add lhs_access_path rhs_attributes attribute_map
let propagate_return_attributes
ret_opt ret_attributes actuals attribute_map ~f_resolve_id formal_map =
match ret_opt with
| Some (ret_id, ret_typ) ->
let ownership_attributes, other_attributes =
Domain.AttributeSetDomain.partition
(function
| OwnedIf _ -> true
| _ -> false)
ret_attributes in
let caller_return_attributes =
match Domain.AttributeSetDomain.elements ownership_attributes with
| [] -> other_attributes
| [(OwnedIf None) as unconditionally_owned] ->
Domain.AttributeSetDomain.add unconditionally_owned other_attributes
| [OwnedIf (Some formal_index)] ->
begin
match List.nth actuals formal_index with
| Some (actual_exp, actual_typ) ->
begin
match
AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id with
| Some actual_ap ->
if is_owned actual_ap attribute_map
then
Domain.AttributeSetDomain.add
Domain.Attribute.unconditionally_owned other_attributes
else
add_conditional_ownership_attribute
actual_ap formal_map attribute_map other_attributes
| None ->
other_attributes
end
| None ->
other_attributes
end
| _multiple_ownership_attributes ->
(* TODO: handle multiple ownership attributes *)
other_attributes in
Domain.AttributeMapDomain.add
(AccessPath.of_id ret_id ret_typ)
caller_return_attributes
attribute_map
| None ->
attribute_map
let add_path_to_state exp typ loc path_state id_map attribute_map tenv =
(* we don't want to warn on writes to the field if it is (a) thread-confined, or (b) volatile *)
let is_safe_write access_path tenv =
let is_thread_safe_write accesses tenv =
match IList.rev accesses,
AccessPath.Raw.get_typ (AccessPath.Raw.truncate access_path) tenv with
| AccessPath.FieldAccess fieldname :: _,
Some (Typ.Tstruct typename | Tptr (Tstruct typename, _)) ->
begin
match Tenv.lookup tenv typename with
| Some struct_typ ->
Annotations.struct_typ_has_annot struct_typ Annotations.ia_is_thread_confined ||
Annotations.field_has_annot
fieldname struct_typ Annotations.ia_is_thread_confined ||
Annotations.field_has_annot fieldname struct_typ Annotations.ia_is_volatile
| None ->
false
end
| _ ->
false in
is_thread_safe_write (snd access_path) tenv in
let f_resolve_id = resolve_id id_map in
if is_constant exp
then
path_state
else
List.fold
~f:(fun acc rawpath ->
if not (is_owned (AccessPath.Raw.truncate rawpath) attribute_map) &&
not (is_safe_write rawpath tenv)
then Domain.PathDomain.add_sink (Domain.make_access rawpath loc) acc
else acc)
~init:path_state
(AccessPath.of_exp exp typ ~f_resolve_id)
let analyze_id_assignment lhs_id rhs_exp rhs_typ { Domain.id_map; } =
let f_resolve_id = resolve_id id_map in
match AccessPath.of_lhs_exp rhs_exp rhs_typ ~f_resolve_id with
| Some rhs_access_path -> IdAccessPathMapDomain.add lhs_id rhs_access_path id_map
| None -> id_map
let has_return_annot predicate pn =
Annotations.pname_has_return_annot
pn
~attrs_of_pname:Specs.proc_resolve_attributes
predicate
let is_functional pname =
let is_annotated_functional =
has_return_annot Annotations.ia_is_functional in
let is_modeled_functional = function
| Procname.Java java_pname ->
begin
match Procname.java_get_class_name java_pname,
Procname.java_get_method java_pname with
| "android.content.res.Resources", method_name ->
(* all methods of Resources are considered @Functional except for the ones in this
blacklist *)
let non_functional_resource_methods = [
"getAssets";
"getConfiguration";
"getSystem";
"newTheme";
"openRawResource";
"openRawResourceFd"
] in
not (List.mem non_functional_resource_methods method_name)
| _ ->
false
end
| _ ->
false in
is_annotated_functional pname || is_modeled_functional pname
let acquires_ownership pname tenv =
let is_allocation pn =
Procname.equal pn BuiltinDecl.__new || Procname.equal pn BuiltinDecl.__new_array in
(* identify library functions that maintain ownership invariants behind the scenes *)
let is_owned_in_library = function
| Procname.Java java_pname ->
begin
match Procname.java_get_class_name java_pname,
Procname.java_get_method java_pname with
| "javax.inject.Provider", "get" ->
(* in dependency injection, the library allocates fresh values behind the scenes *)
true
| "java.lang.ThreadLocal", "get" ->
(* ThreadLocal prevents sharing between threads behind the scenes *)
true
| "android.support.v4.util.Pools$SynchronizedPool", "acquire" ->
(* a pool should own all of its objects *)
true
| _ ->
false
end
| _ ->
false in
is_allocation pname ||
is_owned_in_library pname ||
PatternMatch.override_exists is_owned_in_library tenv pname
let exec_instr (astate : Domain.astate) { ProcData.pdesc; tenv; extras; } _ =
let is_container_write pn tenv = match pn with
| Procname.Java java_pname ->
let typename = Typename.Java.from_string (Procname.java_get_class_name java_pname) in
let is_container_write_ typename _ =
match Typename.name typename, Procname.java_get_method java_pname with
| "java.util.List", ("add" | "addAll" | "clear" | "remove" | "set") -> true
| "java.util.Map", ("clear" | "put" | "putAll" | "remove") -> true
| _ -> false in
let is_threadsafe_collection typename _ = match Typename.name typename with
| "java.util.concurrent.ConcurrentMap" | "java.util.concurrent.CopyOnWriteArrayList" ->
true
| _ ->
false in
PatternMatch.supertype_exists tenv is_container_write_ typename &&
not (PatternMatch.supertype_exists tenv is_threadsafe_collection typename)
| _ -> false in
let add_container_write callee_pname actuals ~f_resolve_id callee_loc =
match actuals with
| (receiver_exp, receiver_typ) :: _ ->
(* create a dummy write that represents mutating the contents of the container *)
let open Domain in
let dummy_fieldname =
Ident.create_fieldname (Mangled.from_string (Procname.get_method callee_pname)) 0 in
let dummy_access_exp = Exp.Lfield (receiver_exp, dummy_fieldname, receiver_typ) in
let callee_conditional_writes =
match AccessPath.of_lhs_exp dummy_access_exp receiver_typ ~f_resolve_id with
| Some container_ap ->
let writes =
PathDomain.add_sink
(make_access container_ap callee_loc)
PathDomain.empty in
ConditionalWritesDomain.add 0 writes ConditionalWritesDomain.empty
| None ->
ConditionalWritesDomain.empty in
Some
(false,
PathDomain.empty,
callee_conditional_writes,
PathDomain.empty,
AttributeSetDomain.empty)
| _ ->
failwithf
"Call to %a is marked as a container write, but has no receiver"
Procname.pp callee_pname in
let get_summary caller_pdesc callee_pname actuals ~f_resolve_id callee_loc tenv =
if is_container_write callee_pname tenv
then
add_container_write callee_pname actuals ~f_resolve_id callee_loc
else
Summary.read_summary caller_pdesc callee_pname in
let is_unprotected is_locked =
not is_locked && not (Procdesc.is_java_synchronized pdesc) in
(* return true if the given procname boxes a primitive type into a reference type *)
let is_box = function
| Procname.Java java_pname ->
begin
match Procname.java_get_class_name java_pname, Procname.java_get_method java_pname with
| ("java.lang.Boolean" |
"java.lang.Byte" |
"java.lang.Char" |
"java.lang.Double" |
"java.lang.Float" |
"java.lang.Integer" |
"java.lang.Long" |
"java.lang.Short"),
"valueOf" -> true
| _ -> false
end
| _ ->
false in
let f_resolve_id = resolve_id astate.id_map in
let open Domain in
function
| Sil.Call (Some (lhs_id, lhs_typ), Const (Cfun pn), _, _, _) when acquires_ownership pn tenv ->
begin
match AccessPath.of_lhs_exp (Exp.Var lhs_id) lhs_typ ~f_resolve_id with
| Some lhs_access_path ->
let attribute_map =
AttributeMapDomain.add_attribute
lhs_access_path
Attribute.unconditionally_owned
astate.attribute_map in
{ astate with attribute_map; }
| None ->
astate
end
| Sil.Call (Some (ret_id, _), Const (Cfun callee_pname),
(target_exp, target_typ) :: (Exp.Sizeof (cast_typ, _, _), _) :: _ , _, _)
when Procname.equal callee_pname BuiltinDecl.__cast ->
let lhs_access_path = AccessPath.of_id ret_id (Typ.Tptr (cast_typ, Pk_pointer)) in
let attribute_map =
propagate_attributes
lhs_access_path target_exp target_typ ~f_resolve_id astate.attribute_map extras in
{ astate with attribute_map; }
| Sil.Call (ret_opt, Const (Cfun callee_pname), actuals, loc, _) ->
let astate_callee =
(* assuming that modeled procedures do not have useful summaries *)
match get_lock_model callee_pname with
| Lock ->
{ astate with locks = true; }
| Unlock ->
{ astate with locks = false; }
| NoEffect ->
match
get_summary pdesc callee_pname actuals ~f_resolve_id loc tenv with
| Some (callee_locks,
callee_reads,
callee_conditional_writes,
callee_unconditional_writes,
return_attributes) ->
let locks' = callee_locks || astate.locks in
let astate' =
if is_unprotected locks'
then
let call_site = CallSite.make callee_pname loc in
(* add the conditional writes rooted in the callee formal at [index] to
the current state *)
let add_conditional_writes
index ((cond_writes, uncond_writes) as acc) (actual_exp, actual_typ) =
if is_constant actual_exp
then
acc
else
try
let callee_cond_writes_for_index' =
let callee_cond_writes_for_index =
ConditionalWritesDomain.find index callee_conditional_writes in
PathDomain.with_callsite callee_cond_writes_for_index call_site in
begin
match AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id with
| Some actual_access_path ->
if is_owned actual_access_path astate.attribute_map
then
(* the actual passed to the current callee is owned. drop all
the conditional writes for that actual, since they're all
safe *)
acc
else
let base = fst actual_access_path in
begin
match FormalMap.get_formal_index base extras with
| Some formal_index ->
(* the actual passed to the current callee is rooted in
a formal. add to conditional writes *)
let conditional_writes' =
try
ConditionalWritesDomain.find
formal_index cond_writes
|> PathDomain.join callee_cond_writes_for_index'
with Not_found ->
callee_cond_writes_for_index' in
let cond_writes' =
ConditionalWritesDomain.add
formal_index conditional_writes' cond_writes in
cond_writes', uncond_writes
| None ->
(* access path not owned and not rooted in a formal. add
to unconditional writes *)
cond_writes,
PathDomain.join
uncond_writes callee_cond_writes_for_index'
end
| _ ->
cond_writes,
PathDomain.join uncond_writes callee_cond_writes_for_index'
end
with Not_found ->
acc in
let conditional_writes, unconditional_writes =
let combined_unconditional_writes =
PathDomain.with_callsite callee_unconditional_writes call_site
|> PathDomain.join astate.unconditional_writes in
List.foldi
~f:add_conditional_writes
~init:(astate.conditional_writes, combined_unconditional_writes)
actuals in
let reads =
PathDomain.with_callsite callee_reads call_site
|> PathDomain.join astate.reads in
{ astate with reads; conditional_writes; unconditional_writes; }
else
astate in
let attribute_map =
propagate_return_attributes
ret_opt
return_attributes
actuals
astate.attribute_map
~f_resolve_id
extras in
{ astate' with locks = locks'; attribute_map; }
| None ->
if is_box callee_pname
then
match ret_opt, actuals with
| Some (ret_id, ret_typ), (actual_exp, actual_typ) :: _ ->
begin
match AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id with
| Some ap
when AttributeMapDomain.has_attribute
ap Functional astate.attribute_map ->
let attribute_map =
AttributeMapDomain.add_attribute
(AccessPath.of_id ret_id ret_typ)
Functional
astate.attribute_map in
{ astate with attribute_map; }
| _ ->
astate
end
| _ ->
astate
else if FbThreadSafety.is_graphql_constructor callee_pname
then
(* assume generated GraphQL code returns ownership *)
match ret_opt with
| Some (ret_id, ret_typ) ->
let attribute_map =
AttributeMapDomain.add_attribute
(AccessPath.of_id ret_id ret_typ)
Attribute.unconditionally_owned
astate.attribute_map in
{ astate with attribute_map; }
| None -> astate
else
astate in
begin
match ret_opt with
| Some (_, (Typ.Tint ILong | Tfloat FDouble)) ->
(* writes to longs and doubles are not guaranteed to be atomic in Java, so don't
bother tracking whether a returned long or float value is functional *)
astate_callee
| Some (ret_id, ret_typ) ->
let add_if_annotated predicate attribute attribute_map =
if PatternMatch.override_exists predicate tenv callee_pname
then
AttributeMapDomain.add_attribute
(AccessPath.of_id ret_id ret_typ) attribute attribute_map
else attribute_map in
let attribute_map =
add_if_annotated is_functional Functional astate_callee.attribute_map
|> add_if_annotated
(has_return_annot Annotations.ia_is_returns_ownership)
Domain.Attribute.unconditionally_owned in
{ astate_callee with attribute_map; }
| _ ->
astate_callee
end
| Sil.Store (Exp.Lvar lhs_pvar, lhs_typ, rhs_exp, _)
when Pvar.is_frontend_tmp lhs_pvar && not (is_constant rhs_exp) ->
let id_map' = analyze_id_assignment (Var.of_pvar lhs_pvar) rhs_exp lhs_typ astate in
{ astate with id_map = id_map'; }
| Sil.Store (lhs_exp, lhs_typ, rhs_exp, loc) ->
let get_formal_index exp typ = match AccessPath.of_lhs_exp exp typ ~f_resolve_id with
| Some (base, _) -> FormalMap.get_formal_index base extras
| None -> None in
let is_marked_functional exp typ attribute_map =
match AccessPath.of_lhs_exp exp typ ~f_resolve_id with
| Some access_path ->
AttributeMapDomain.has_attribute access_path Functional attribute_map
| None ->
false in
let conditional_writes, unconditional_writes =
match lhs_exp with
| Lfield (base_exp, _, typ)
when is_unprotected astate.locks (* abstracts no lock being held *) &&
not (is_marked_functional rhs_exp lhs_typ astate.attribute_map) ->
begin
match get_formal_index base_exp typ with
| Some formal_index ->
let conditional_writes_for_index =
try ConditionalWritesDomain.find formal_index astate.conditional_writes
with Not_found -> PathDomain.empty in
let conditional_writes_for_index' =
add_path_to_state
lhs_exp
typ
loc
conditional_writes_for_index
astate.id_map
astate.attribute_map
tenv in
ConditionalWritesDomain.add
formal_index conditional_writes_for_index' astate.conditional_writes,
astate.unconditional_writes
| None ->
astate.conditional_writes,
add_path_to_state
lhs_exp
typ
loc
astate.unconditional_writes
astate.id_map
astate.attribute_map
tenv
end
| _ ->
astate.conditional_writes, astate.unconditional_writes in
let attribute_map =
match AccessPath.of_lhs_exp lhs_exp lhs_typ ~f_resolve_id with
| Some lhs_access_path ->
propagate_attributes
lhs_access_path rhs_exp lhs_typ ~f_resolve_id astate.attribute_map extras
| None ->
astate.attribute_map in
{ astate with conditional_writes; unconditional_writes; attribute_map; }
| Sil.Load (lhs_id, rhs_exp, rhs_typ, loc) ->
let id_map = analyze_id_assignment (Var.of_id lhs_id) rhs_exp rhs_typ astate in
let reads =
match rhs_exp with
| Lfield ( _, _, typ) when is_unprotected astate.locks ->
add_path_to_state rhs_exp typ loc astate.reads astate.id_map astate.attribute_map tenv
| _ ->
astate.reads in
let lhs_access_path = AccessPath.of_id lhs_id rhs_typ in
let attribute_map =
propagate_attributes
lhs_access_path rhs_exp rhs_typ ~f_resolve_id astate.attribute_map extras in
{ astate with Domain.reads; id_map; attribute_map; }
| Sil.Remove_temps (ids, _) ->
let id_map =
List.fold
~f:(fun acc id -> IdAccessPathMapDomain.remove (Var.of_id id) acc)
~init:astate.id_map
ids in
{ astate with id_map; }
| _ ->
astate
end
module Analyzer = AbstractInterpreter.Make (ProcCfg.Normal) (TransferFunctions)
module Interprocedural = AbstractInterpreter.Interprocedural (Summary)
(* a results table is a Map where a key is an a procedure environment,
i.e., something of type Idenv.t * Tenv.t * Procname.t * Procdesc.t
*)
module ResultsTableType = Caml.Map.Make (struct
type t = Idenv.t * Tenv.t * Procname.t * Procdesc.t
let compare (_, _, pn1, _) (_,_,pn2,_) = Procname.compare pn1 pn2
end)
(* we want to consider Builder classes and other safe immutablility-ensuring patterns as
thread-safe. we are overly friendly about this for now; any class whose name ends with `Builder`
is assumed to be thread-safe. in the future, we can ask for builder classes to be annotated with
@Builder and verify that annotated classes satisfy the expected invariants. *)
let is_builder_class class_name =
String.is_suffix ~suffix:"Builder" class_name
(* similarly, we assume that immutable classes safely encapsulate their state *)
let is_immutable_collection_class class_name tenv =
let immutable_collections = [
"com.google.common.collect.ImmutableCollection";
"com.google.common.collect.ImmutableMap";
"com.google.common.collect.ImmutableTable";
] in
PatternMatch.supertype_exists
tenv
(fun typename _ ->
List.mem ~equal:String.equal immutable_collections (Typename.name typename))
class_name
let is_call_to_builder_class_method = function
| Procname.Java java_pname -> is_builder_class (Procname.java_get_class_name java_pname)
| _ -> false
let is_call_to_immutable_collection_method tenv = function
| Procname.Java java_pname ->
is_immutable_collection_class (Procname.java_get_class_type_name java_pname) tenv
| _ ->
false
(* Methods in @ThreadConfined classes and methods annotated with @ThreadConfied are assumed to all
run on the same thread. For the moment we won't warn on accesses resulting from use of such
methods at all. In future we should account for races between these methods and methods from
completely different classes that don't necessarily run on the same thread as the confined
object. *)
let is_thread_confined_method tenv pdesc =
Annotations.pdesc_return_annot_ends_with pdesc Annotations.thread_confined ||
PatternMatch.check_current_class_attributes
Annotations.ia_is_thread_confined tenv (Procdesc.get_proc_name pdesc)
(* we don't want to warn on methods that run on the UI thread because they should always be
single-threaded *)
let runs_on_ui_thread proc_desc =
(* assume that methods annotated with @UiThread, @OnEvent, @OnBind, @OnMount, @OnUnbind,
@OnUnmount always run on the UI thread *)
Annotations.pdesc_has_return_annot
proc_desc
(fun annot -> Annotations.ia_is_ui_thread annot ||
Annotations.ia_is_on_bind annot ||
Annotations.ia_is_on_event annot ||
Annotations.ia_is_on_mount annot ||
Annotations.ia_is_on_unbind annot ||
Annotations.ia_is_on_unmount annot)
let is_assumed_thread_safe pdesc =
Annotations.pdesc_return_annot_ends_with pdesc Annotations.assume_thread_safe
(* return true if we should compute a summary for the procedure. if this returns false, we won't
analyze the procedure or report any warnings on it *)
(* note: in the future, we will want to analyze the procedures in all of these cases in order to
find more bugs. this is just a temporary measure to avoid obvious false positives *)
let should_analyze_proc pdesc tenv =
let pn = Procdesc.get_proc_name pdesc in
not (Procname.is_class_initializer pn) &&
not (FbThreadSafety.is_logging_method pn) &&
not (is_call_to_builder_class_method pn) &&
not (is_call_to_immutable_collection_method tenv pn) &&
not (runs_on_ui_thread pdesc) &&
not (is_thread_confined_method tenv pdesc) &&
not (is_assumed_thread_safe pdesc)
(* return true if we should report on unprotected accesses during the procedure *)
let should_report_on_proc (_, _, proc_name, proc_desc) =
not (Procname.java_is_autogen_method proc_name) &&
Procdesc.get_access proc_desc <> PredSymb.Private &&
not (Annotations.pdesc_return_annot_ends_with proc_desc Annotations.visibleForTesting)
(* creates a map from proc_envs to postconditions *)
let make_results_table get_proc_desc file_env =
(* make a Map sending each element e of list l to (f e) *)
let map_post_computation_over_procs f l =
List.fold
~f:(fun m p -> ResultsTableType.add p (f p) m)
~init:ResultsTableType.empty
l in
let is_initializer tenv proc_name =
Procname.is_constructor proc_name || FbThreadSafety.is_custom_init tenv proc_name in
let compute_post_for_procedure = (* takes proc_env as arg *)
fun (idenv, tenv, proc_name, proc_desc) ->
let open ThreadSafetyDomain in
let has_lock = false in
let return_attrs = AttributeSetDomain.empty in
let empty =
has_lock, PathDomain.empty, ConditionalWritesDomain.empty, PathDomain.empty, return_attrs in
(* convert the abstract state to a summary by dropping the id map *)
let compute_post ({ ProcData.pdesc; tenv; extras; } as proc_data) =
if should_analyze_proc pdesc tenv
then
begin
if not (Procdesc.did_preanalysis pdesc) then Preanal.do_liveness pdesc tenv;
let initial =
if is_initializer tenv (Procdesc.get_proc_name pdesc)
then
(* express that the constructor owns [this] *)
match FormalMap.get_formal_base 0 extras with
| Some base ->
let attribute_map =
AttributeMapDomain.add_attribute
(base, [])
Attribute.unconditionally_owned
ThreadSafetyDomain.empty.attribute_map in
{ ThreadSafetyDomain.empty with attribute_map; }
| None -> ThreadSafetyDomain.empty
else
ThreadSafetyDomain.empty in
match Analyzer.compute_post proc_data ~initial with
| Some { locks; reads; conditional_writes; unconditional_writes; attribute_map; } ->
let return_var_ap =
AccessPath.of_pvar
(Pvar.get_ret_pvar (Procdesc.get_proc_name pdesc))
(Procdesc.get_ret_type pdesc) in
let return_attributes =
try AttributeMapDomain.find return_var_ap attribute_map
with Not_found -> AttributeSetDomain.empty in
Some (locks, reads, conditional_writes, unconditional_writes, return_attributes)
| None ->
None
end
else
Some empty in
let callback_arg =
let get_procs_in_file _ = [] in
{ Callbacks.get_proc_desc; get_procs_in_file; idenv; tenv; proc_name; proc_desc } in
match
Interprocedural.compute_and_store_post
~compute_post
~make_extras:FormalMap.make
callback_arg with
| Some post -> post
| None -> empty
in
map_post_computation_over_procs compute_post_for_procedure file_env
let get_current_class_and_threadsafe_superclasses tenv pname =
match pname with
| Procname.Java java_pname ->
let current_class = Procname.java_get_class_type_name java_pname in
let thread_safe_annotated_classes = PatternMatch.find_superclasses_with_attributes
Annotations.ia_is_thread_safe tenv current_class
in
Some (current_class,thread_safe_annotated_classes)
| _ -> None (*shouldn't happen*)
(** The addendum message says that a superclass is marked @ThreadSafe,
when the current class is not so marked*)
let calculate_addendum_message tenv pname =
match get_current_class_and_threadsafe_superclasses tenv pname with
| Some (current_class,thread_safe_annotated_classes) ->
if not (List.mem ~equal:Typename.equal thread_safe_annotated_classes current_class) then
match thread_safe_annotated_classes with
| hd::_ -> F.asprintf "\n Note: Superclass %a is marked @ThreadSafe." Typename.pp hd
| [] -> ""
else ""
| _ -> ""
let combine_conditional_unconditional_writes conditional_writes unconditional_writes =
let open ThreadSafetyDomain in
ConditionalWritesDomain.fold
(fun _ writes acc -> PathDomain.join writes acc)
conditional_writes
unconditional_writes
let equal_accesses (sink1 : ThreadSafetyDomain.TraceElem.t)
(sink2 : ThreadSafetyDomain.TraceElem.t) =
AccessPath.equal_access_list
(snd (ThreadSafetyDomain.TraceElem.kind sink1))
(snd (ThreadSafetyDomain.TraceElem.kind sink2))
(* For now equal-access and conflicting-access are equivalent.
But that will change when we (soon) consider conficting accesses
that are not via assignment, such as add and get for containers*)
let conflicting_accesses (sink1 : ThreadSafetyDomain.TraceElem.t)
(sink2 : ThreadSafetyDomain.TraceElem.t) =
equal_accesses sink1 sink2
(* trace is really reads or writes set. Fix terminology later *)
let filter_conflicting_sinks sink trace =
let conflicts =
ThreadSafetyDomain.PathDomain.Sinks.filter
(fun sink2 -> conflicting_accesses sink sink2)
(ThreadSafetyDomain.PathDomain.sinks trace) in
ThreadSafetyDomain.PathDomain.update_sinks trace conflicts
(* Given a sink representing a read access,
return a list of (proc_env,access-astate) pairs where
access-astate is a non-empty collection of conflicting
write accesses*)
let collect_conflicting_writes sink tab =
let procs_and_writes =
List.map
~f:(fun (key,(_, _, conditional_writes, unconditional_writes, _)) ->
let conflicting_writes =
combine_conditional_unconditional_writes
conditional_writes unconditional_writes
|> filter_conflicting_sinks sink in
key, conflicting_writes
)
(ResultsTableType.bindings tab) in
List.filter
~f:(fun (proc_env,writes) ->
(should_report_on_proc proc_env)
&& not (ThreadSafetyDomain.PathDomain.Sinks.is_empty
(ThreadSafetyDomain.PathDomain.sinks writes))
)
procs_and_writes
(* keep only the first copy of an access per procedure *)
let de_dup trace =
let original_sinks = ThreadSafetyDomain.PathDomain.sinks trace in
let list_of_original_sinks = ThreadSafetyDomain.PathDomain.Sinks.elements original_sinks in
let de_duped_sinks =
ThreadSafetyDomain.PathDomain.Sinks.filter
(fun sink ->
(* for each sink we will keep one in the equivalence class of those
with same access path. We select that by using find_exn to get
the first element equivalent ot sink in a list of sinks. This
first element is the dedup representative, and it happens to
typically be the first such access in a method. *)
let first_sink =
List.find_exn
~f:(fun sink2 -> equal_accesses sink sink2)
list_of_original_sinks in
Int.equal (ThreadSafetyDomain.TraceElem.compare sink first_sink) 0
)
original_sinks in
ThreadSafetyDomain.PathDomain.update_sinks trace de_duped_sinks
(*A helper function used in the error reporting*)
let pp_accesses_sink fmt sink =
let _, accesses = ThreadSafetyDomain.PathDomain.Sink.kind sink in
AccessPath.pp_access_list fmt accesses
(* trace is really a set of accesses*)
let report_thread_safety_violations ( _, tenv, pname, pdesc) make_description trace tab =
let open ThreadSafetyDomain in
let trace_of_pname callee_pname =
match Summary.read_summary pdesc callee_pname with
| Some (_, _, conditional_writes, unconditional_writes, _) ->
combine_conditional_unconditional_writes conditional_writes unconditional_writes
| _ ->
PathDomain.empty in
let report_one_path ((_, sinks) as path) =
let initial_sink, _ = List.last_exn sinks in
let final_sink, _ = List.hd_exn sinks in
let initial_sink_site = PathDomain.Sink.call_site initial_sink in
let final_sink_site = PathDomain.Sink.call_site final_sink in
let desc_of_sink sink =
if CallSite.equal (PathDomain.Sink.call_site sink) final_sink_site
then
Format.asprintf "access to %a" pp_accesses_sink sink
else
Format.asprintf
"call to %a" Procname.pp (CallSite.pname (PathDomain.Sink.call_site sink)) in
let loc = CallSite.loc (PathDomain.Sink.call_site initial_sink) in
let ltr = PathDomain.to_sink_loc_trace ~desc_of_sink path in
let msg = Localise.to_string Localise.thread_safety_violation in
let description = make_description tenv pname final_sink_site
initial_sink_site final_sink tab in
let exn = Exceptions.Checkers (msg, Localise.verbatim_desc description) in
Reporting.log_error pname ~loc ~ltr exn in
List.iter
~f:report_one_path
(PathDomain.get_reportable_sink_paths (de_dup trace) ~trace_of_pname)
let make_unprotected_write_description
tenv pname final_sink_site initial_sink_site final_sink _ =
Format.asprintf
"Unprotected write. Public method %a%s writes to field %a outside of synchronization.%s"
Procname.pp pname
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
pp_accesses_sink final_sink
(calculate_addendum_message tenv pname)
let make_read_write_race_description tenv pname final_sink_site initial_sink_site final_sink tab =
let conflicting_proc_envs = List.map
~f:fst
(collect_conflicting_writes final_sink tab) in
let conflicting_proc_names = List.map
~f:(fun (_,_,proc_name,_) -> proc_name)
conflicting_proc_envs in
let pp_proc_name_list fmt proc_names =
let pp_sep _ _ = F.fprintf fmt " , " in
F.pp_print_list ~pp_sep Procname.pp fmt proc_names in
let conflicts_description =
Format.asprintf "Potentially races with writes in method%s %a."
(if List.length conflicting_proc_names > 1 then "s" else "")
pp_proc_name_list conflicting_proc_names in
Format.asprintf "Read/Write race. Public method %a%s reads from field %a. %s %s"
Procname.pp pname
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
pp_accesses_sink final_sink
conflicts_description
(calculate_addendum_message tenv pname)
(* find those elements of reads which have conflicts
somewhere else, and report them *)
let report_reads proc_env reads tab =
let racy_read_sinks =
ThreadSafetyDomain.PathDomain.Sinks.filter
(fun sink ->
(* there exists a postcondition whose write set conflicts with
sink*)
not (List.is_empty (collect_conflicting_writes sink tab))
)
(ThreadSafetyDomain.PathDomain.sinks reads)
in
let racy_reads =
ThreadSafetyDomain.PathDomain.update_sinks reads racy_read_sinks
in
report_thread_safety_violations proc_env
make_read_write_race_description
racy_reads
tab
(* Currently we analyze if there is an @ThreadSafe annotation on at least one of
the classes in a file. This might be tightened in future or even broadened in future
based on other criteria *)
let should_report_on_file file_env =
let current_class_or_super_marked_threadsafe =
fun (_, tenv, pname, _) ->
match get_current_class_and_threadsafe_superclasses tenv pname with
| Some (_, thread_safe_annotated_classes) ->
not (List.is_empty thread_safe_annotated_classes)
| _ -> false
in
let current_class_marked_not_threadsafe =
fun (_, tenv, pname, _) ->
PatternMatch.check_current_class_attributes Annotations.ia_is_not_thread_safe tenv pname
in
not (List.exists ~f:current_class_marked_not_threadsafe file_env) &&
List.exists ~f:current_class_or_super_marked_threadsafe file_env
(* For now, just checks if there is one active element amongst the posts of the analyzed methods.
This indicates that the method races with itself. To be refined later. *)
let process_results_table file_env tab =
let should_report_on_all_procs = should_report_on_file file_env in
(* TODO (t15588153): clean this up *)
let is_thread_safe_method pdesc tenv =
PatternMatch.override_exists
(fun pn ->
Annotations.pname_has_return_annot
pn
~attrs_of_pname:Specs.proc_resolve_attributes
Annotations.ia_is_thread_safe_method)
tenv
(Procdesc.get_proc_name pdesc) in
let should_report ((_, tenv, _, pdesc) as proc_env) =
(should_report_on_all_procs || is_thread_safe_method pdesc tenv)
&& should_report_on_proc proc_env in
ResultsTableType.iter (* report errors for each method *)
(fun proc_env (_, reads, conditional_writes, unconditional_writes, _) ->
if should_report proc_env then
let writes = combine_conditional_unconditional_writes
conditional_writes unconditional_writes in
begin
report_thread_safety_violations
proc_env make_unprotected_write_description writes tab
; report_reads proc_env reads tab
end
)
tab
(*This is a "cluster checker" *)
(*Gathers results by analyzing all the methods in a file, then post-processes
the results to check (approximation of) thread safety *)
(* file_env: (Idenv.t * Tenv.t * Procname.t * Procdesc.t) list *)
let file_analysis _ _ get_procdesc file_env =
process_results_table file_env (make_results_table get_procdesc file_env)