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.
1480 lines
64 KiB
1480 lines
64 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 MF = MarkupFormatter
|
|
|
|
module Summary = Summary.Make (struct
|
|
type payload = ThreadSafetyDomain.summary
|
|
|
|
let update_payload post (summary : Specs.summary) =
|
|
{ summary with payload = { summary.payload with threadsafety = Some post }}
|
|
|
|
let read_payload (summary : Specs.summary) =
|
|
summary.payload.threadsafety
|
|
end)
|
|
|
|
let is_owned access_path attribute_map =
|
|
ThreadSafetyDomain.AttributeMapDomain.has_attribute
|
|
access_path ThreadSafetyDomain.Attribute.unconditionally_owned attribute_map
|
|
|
|
let container_write_string = "infer.dummy.__CONTAINERWRITE__"
|
|
|
|
(* return (name of container, name of mutating function call) pair *)
|
|
let get_container_write_desc sink =
|
|
let (base_var, _), access_list = fst (ThreadSafetyDomain.TraceElem.kind sink) in
|
|
let get_container_write_desc_ call_name container_name =
|
|
match String.chop_prefix (Fieldname.to_string call_name) ~prefix:container_write_string with
|
|
| Some call_name -> Some (container_name, call_name)
|
|
| None -> None in
|
|
|
|
match List.rev access_list with
|
|
| FieldAccess call_name :: FieldAccess container_name :: _ ->
|
|
get_container_write_desc_ call_name (Fieldname.to_string container_name)
|
|
| [FieldAccess call_name] ->
|
|
get_container_write_desc_ call_name (F.asprintf "%a" Var.pp base_var)
|
|
| _ ->
|
|
None
|
|
|
|
let is_container_write_sink sink =
|
|
Option.is_some (get_container_write_desc sink)
|
|
|
|
(*Bit of redundancy with code in is_unprotected, might alter later *)
|
|
let make_excluder locks threads =
|
|
if locks && not threads then ThreadSafetyDomain.Excluder.Lock
|
|
else if not locks && threads then ThreadSafetyDomain.Excluder.Thread
|
|
else if locks && threads then ThreadSafetyDomain.Excluder.Both
|
|
else failwithf "called when neither lock nor thread known"
|
|
|
|
module TransferFunctions (CFG : ProcCfg.S) = struct
|
|
module CFG = CFG
|
|
module Domain = ThreadSafetyDomain
|
|
type extras = FormalMap.t
|
|
|
|
type lock_model =
|
|
| Lock
|
|
| Unlock
|
|
| LockedIfTrue
|
|
| NoEffect
|
|
|
|
type thread_model =
|
|
| Threaded
|
|
| Unknown
|
|
| ThreadedIfTrue
|
|
|
|
let is_thread_utils_type java_pname =
|
|
let pn = (Typ.Procname.java_get_class_name java_pname) in
|
|
String.is_suffix ~suffix:"ThreadUtils" pn
|
|
|| String.is_suffix ~suffix:"ThreadUtil" pn
|
|
|
|
let is_thread_utils_method method_name_str = function
|
|
| Typ.Procname.Java java_pname ->
|
|
is_thread_utils_type java_pname
|
|
&& String.equal (Typ.Procname.java_get_method java_pname) method_name_str
|
|
| _ -> false
|
|
|
|
let get_lock_model =
|
|
let is_cpp_lock =
|
|
let matcher = QualifiedCppName.Match.of_fuzzy_qual_names [
|
|
"std::mutex::lock"; "std::lock_guard::lock_guard"] in
|
|
fun pname ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
and is_std_mutex_unlock =
|
|
let matcher = QualifiedCppName.Match.of_fuzzy_qual_names ["std::mutex::unlock"] in
|
|
fun pname ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
in
|
|
function
|
|
| Typ.Procname.Java java_pname ->
|
|
if is_thread_utils_method "assertHoldsLock" (Typ.Procname.Java java_pname) then Lock
|
|
else
|
|
begin
|
|
match Typ.Procname.java_get_class_name java_pname,
|
|
Typ.Procname.java_get_method java_pname with
|
|
| ("java.util.concurrent.locks.Lock"
|
|
| "java.util.concurrent.locks.ReentrantLock"
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock"),
|
|
("lock" | "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
|
|
| ("java.util.concurrent.locks.Lock"
|
|
| "java.util.concurrent.locks.ReentrantLock"
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock"),
|
|
"tryLock" ->
|
|
LockedIfTrue
|
|
| "com.facebook.buck.util.concurrent.AutoCloseableReadWriteUpdateLock",
|
|
("readLock" | "updateLock" | "writeLock") ->
|
|
Lock
|
|
| _ ->
|
|
NoEffect
|
|
end
|
|
| (Typ.Procname.ObjC_Cpp _ as pname) when is_cpp_lock pname ->
|
|
Lock
|
|
| (Typ.Procname.ObjC_Cpp _ as pname) when is_std_mutex_unlock pname ->
|
|
Unlock
|
|
| pname when Typ.Procname.equal pname BuiltinDecl.__set_locked_attribute ->
|
|
Lock
|
|
| pname when Typ.Procname.equal pname BuiltinDecl.__delete_locked_attribute ->
|
|
Unlock
|
|
| _ ->
|
|
NoEffect
|
|
|
|
let get_thread_model = function
|
|
| Typ.Procname.Java java_pname ->
|
|
if is_thread_utils_type java_pname then
|
|
match Typ.Procname.java_get_method java_pname with
|
|
| "assertMainThread"
|
|
| "checkOnMainThread"
|
|
| "assertOnUiThread" -> Threaded
|
|
| "isMainThread"
|
|
| "isUiThread" -> ThreadedIfTrue
|
|
| _ -> Unknown
|
|
else Unknown
|
|
(*Note we are not modelling assertOnNonUiThread or
|
|
assertOnBackgroundThread. These treated as Unknown*)
|
|
| _ -> Unknown
|
|
|
|
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
|
|
|
|
|
|
let remove_ownership_attributes attributes =
|
|
Domain.AttributeSetDomain.filter
|
|
(function | Domain.Attribute.OwnedIf _ -> false | _ -> true)
|
|
attributes
|
|
|
|
(* propagate attributes from the leaves to the root of an RHS Hil expression.
|
|
Special casing on ownership. *)
|
|
let rec attributes_of_expr formal_map attribute_map e =
|
|
let open HilExp in
|
|
let open Domain in
|
|
match e with
|
|
| Constant _ ->
|
|
AttributeSetDomain.of_list [Attribute.unconditionally_owned; Attribute.Functional]
|
|
| AccessPath ap ->
|
|
begin
|
|
try AttributeMapDomain.find ap attribute_map
|
|
with Not_found -> AttributeSetDomain.empty
|
|
end
|
|
|> add_conditional_ownership_attribute ap formal_map attribute_map
|
|
| Exception expr (* treat exceptions as transparent wrt attributes *)
|
|
| Cast(_, expr) ->
|
|
attributes_of_expr formal_map attribute_map expr
|
|
| UnaryOperator(_, expr, _) ->
|
|
attributes_of_expr formal_map attribute_map expr
|
|
|> remove_ownership_attributes
|
|
| BinaryOperator(_, expr1, expr2) ->
|
|
let attributes1 = attributes_of_expr formal_map attribute_map expr1 in
|
|
let attributes2 = attributes_of_expr formal_map attribute_map expr2 in
|
|
AttributeSetDomain.join attributes1 attributes2
|
|
|> remove_ownership_attributes
|
|
| Closure _ | Sizeof _ ->
|
|
AttributeSetDomain.empty
|
|
|
|
(* will return true on x.f.g.h when x.f and x.f.g are owned, but not requiring x.f.g.h *)
|
|
(* must not be called with an empty access list *)
|
|
let all_prefixes_owned (base, accesses) attribute_map =
|
|
let but_last_rev = List.rev accesses |> List.tl_exn in
|
|
let rec aux acc = function
|
|
| [] -> acc
|
|
| (_::tail) as all -> aux ((List.rev all)::acc) tail in
|
|
let prefixes = aux [] but_last_rev in
|
|
List.for_all ~f:(fun ap -> is_owned (base, ap) attribute_map) prefixes
|
|
|
|
let propagate_attributes lhs_access_path rhs_exp attribute_map formal_map =
|
|
let rhs_attributes = attributes_of_expr formal_map attribute_map rhs_exp in
|
|
let lhs_root = fst lhs_access_path in
|
|
let filter_on_globals =
|
|
(* do not assign ownership to access paths rooted at globals *)
|
|
if Var.is_global (fst lhs_root)
|
|
then remove_ownership_attributes else Fn.id
|
|
in
|
|
let filter_on_lhs =
|
|
(* do not assign ownership when lhs is not a single var or
|
|
a single-field ap whose root is conditionally owned, or,
|
|
all prefixes are owned *)
|
|
match snd lhs_access_path with
|
|
| [] -> Fn.id
|
|
| [_] when FormalMap.is_formal lhs_root formal_map -> Fn.id
|
|
| _ when all_prefixes_owned lhs_access_path attribute_map -> Fn.id
|
|
| _ -> remove_ownership_attributes
|
|
in
|
|
let final_attributes = filter_on_globals rhs_attributes |> filter_on_lhs in
|
|
Domain.AttributeMapDomain.add lhs_access_path final_attributes attribute_map
|
|
|
|
let propagate_return_attributes ret_opt ret_attributes actuals attribute_map formal_map =
|
|
let open Domain in
|
|
match ret_opt with
|
|
| None ->
|
|
attribute_map
|
|
| Some ret ->
|
|
let ownership_attributes, other_attributes =
|
|
AttributeSetDomain.partition (function | OwnedIf _ -> true | _ -> false) ret_attributes in
|
|
let caller_return_attributes =
|
|
match AttributeSetDomain.elements ownership_attributes with
|
|
| [] -> other_attributes
|
|
| [(OwnedIf None) as unconditionally_owned] ->
|
|
AttributeSetDomain.add unconditionally_owned other_attributes
|
|
| [OwnedIf (Some formal_index)] ->
|
|
begin
|
|
match List.nth actuals formal_index with
|
|
| Some (HilExp.AccessPath actual_ap) ->
|
|
if is_owned actual_ap attribute_map
|
|
then
|
|
AttributeSetDomain.add Attribute.unconditionally_owned other_attributes
|
|
else
|
|
add_conditional_ownership_attribute
|
|
actual_ap formal_map attribute_map other_attributes
|
|
| Some (HilExp.Constant _) ->
|
|
AttributeSetDomain.add Attribute.unconditionally_owned other_attributes
|
|
| _ ->
|
|
other_attributes
|
|
end
|
|
| _multiple_ownership_attributes ->
|
|
(* TODO: handle multiple ownership attributes *)
|
|
other_attributes in
|
|
AttributeMapDomain.add (ret, []) caller_return_attributes attribute_map
|
|
|
|
let is_unprotected is_locked is_threaded pdesc =
|
|
not is_locked && not is_threaded
|
|
&& not (Procdesc.is_java_synchronized pdesc)
|
|
|
|
|
|
let add_access
|
|
exp loc access_kind accesses locks threads
|
|
attribute_map (proc_data : FormalMap.t ProcData.t) =
|
|
let open Domain in
|
|
(* we don't want to warn on accesses to the field if it is (a) thread-confined, or
|
|
(b) volatile *)
|
|
let is_safe_access access prefix_path tenv =
|
|
match access, AccessPath.Raw.get_typ prefix_path tenv with
|
|
| AccessPath.FieldAccess fieldname,
|
|
Some ({Typ.desc=Tstruct typename} | {desc=Tptr ({desc=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
|
|
let rec add_field_accesses pre prefix_path access_acc = function
|
|
| [] ->
|
|
access_acc
|
|
| access :: access_list' ->
|
|
let kind =
|
|
if List.is_empty access_list'
|
|
then access_kind
|
|
else ThreadSafetyDomain.Access.Read in
|
|
let access_path = fst prefix_path, (snd prefix_path) @ [access] in
|
|
let access_acc' =
|
|
if is_owned prefix_path attribute_map ||
|
|
is_safe_access access prefix_path proc_data.tenv
|
|
then
|
|
access_acc
|
|
else
|
|
(* TODO: I think there's a utility function for this somewhere *)
|
|
let accesses = AccessDomain.get_accesses pre access_acc in
|
|
let accesses' = PathDomain.add_sink (make_access access_path kind loc) accesses in
|
|
AccessDomain.add pre accesses' access_acc in
|
|
add_field_accesses pre access_path access_acc' access_list' in
|
|
let add_access_ acc (base, accesses) =
|
|
if List.is_empty accesses
|
|
then acc
|
|
else
|
|
let pre =
|
|
if is_unprotected locks threads proc_data.pdesc
|
|
then
|
|
match FormalMap.get_formal_index base proc_data.extras with
|
|
| Some formal_index -> AccessPrecondition.Unprotected (Some formal_index)
|
|
| None -> AccessPrecondition.unprotected
|
|
else
|
|
AccessPrecondition.Protected
|
|
(make_excluder (locks || Procdesc.is_java_synchronized proc_data.pdesc)
|
|
threads) in
|
|
add_field_accesses pre (base, []) acc accesses in
|
|
|
|
List.fold ~f:add_access_ ~init:accesses (HilExp.get_access_paths exp)
|
|
|
|
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
|
|
| Typ.Procname.Java java_pname ->
|
|
begin
|
|
match Typ.Procname.java_get_class_name java_pname,
|
|
Typ.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 =
|
|
Typ.Procname.equal pn BuiltinDecl.__new || Typ.Procname.equal pn BuiltinDecl.__new_array in
|
|
(* identify library functions that maintain ownership invariants behind the scenes *)
|
|
let is_owned_in_library = function
|
|
| Typ.Procname.Java java_pname ->
|
|
begin
|
|
match Typ.Procname.java_get_class_name java_pname,
|
|
Typ.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.Class" | "java.lang.reflect.Constructor"), "newInstance" ->
|
|
(* reflection can perform allocations *)
|
|
true
|
|
| "java.lang.Object" , "clone" ->
|
|
(* cloning is like allocation *)
|
|
true
|
|
| "java.lang.ThreadLocal", "get" ->
|
|
(* ThreadLocal prevents sharing between threads behind the scenes *)
|
|
true
|
|
| ("android.app.Activity" | "android.view.View"), "findViewById" ->
|
|
(* assume findViewById creates fresh View's (note: not always true) *)
|
|
true
|
|
| "android.support.v4.util.Pools$Pool", "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 is_container_write pn tenv = match pn with
|
|
| Typ.Procname.Java java_pname ->
|
|
let typename = Typ.Name.Java.from_string (Typ.Procname.java_get_class_name java_pname) in
|
|
let is_container_write_ typename _ =
|
|
match Typ.Name.name typename, Typ.Procname.java_get_method java_pname with
|
|
| ("android.util.SparseArray" | "android.support.v4.util.SparseArrayCompat"),
|
|
("append" | "clear" | "delete" | "put" | "remove" | "removeAt" | "removeAtRange"
|
|
| "setValueAt") -> true
|
|
| "android.support.v4.util.SimpleArrayMap",
|
|
("clear" | "ensureCapacity" | "put" | "putAll" | "remove" | "removeAt"
|
|
| "setValueAt") -> true
|
|
| "android.support.v4.util.Pools$SimplePool",
|
|
("acquire" | "release") -> true
|
|
| "java.util.List", ("add" | "addAll" | "clear" | "remove" | "set") -> true
|
|
| "java.util.Map", ("clear" | "put" | "putAll" | "remove") -> true
|
|
| _ -> false in
|
|
PatternMatch.supertype_exists tenv is_container_write_ typename
|
|
| _ ->
|
|
false
|
|
|
|
let is_threadsafe_collection pn tenv = match pn with
|
|
| Typ.Procname.Java java_pname ->
|
|
let typename = Typ.Name.Java.from_string (Typ.Procname.java_get_class_name java_pname) in
|
|
let aux tn _ =
|
|
match Typ.Name.name tn with
|
|
| "java.util.concurrent.ConcurrentMap"
|
|
| "java.util.concurrent.CopyOnWriteArrayList"
|
|
| "android.support.v4.util.Pools$SynchronizedPool" -> true
|
|
| _ -> false in
|
|
PatternMatch.supertype_exists tenv aux typename
|
|
| _ -> false
|
|
|
|
let is_synchronized_container callee_pname ((_, (base_typ : Typ.t)), accesses) tenv =
|
|
if is_threadsafe_collection callee_pname tenv
|
|
then
|
|
true
|
|
else
|
|
let is_annotated_synchronized base_typename container_field tenv =
|
|
match Tenv.lookup tenv base_typename with
|
|
| Some base_typ ->
|
|
Annotations.field_has_annot
|
|
container_field
|
|
base_typ Annotations.ia_is_synchronized_collection
|
|
| None ->
|
|
false in
|
|
match List.rev accesses with
|
|
| AccessPath.FieldAccess base_field ::
|
|
AccessPath.FieldAccess container_field :: _->
|
|
let base_typename =
|
|
Typ.Name.Java.from_string (Fieldname.java_get_class base_field) in
|
|
is_annotated_synchronized base_typename container_field tenv
|
|
| [AccessPath.FieldAccess container_field] ->
|
|
begin
|
|
match base_typ.desc with
|
|
| Typ.Tstruct base_typename | Tptr ({Typ.desc=Tstruct base_typename}, _) ->
|
|
is_annotated_synchronized base_typename container_field tenv
|
|
| _ ->
|
|
false
|
|
end
|
|
| _ ->
|
|
false
|
|
|
|
let make_container_write callee_pname receiver_ap callee_loc tenv =
|
|
(* create a dummy write that represents mutating the contents of the container *)
|
|
let open Domain in
|
|
let callee_accesses =
|
|
if is_synchronized_container callee_pname receiver_ap tenv
|
|
then
|
|
AccessDomain.empty
|
|
else
|
|
let dummy_fieldname =
|
|
Fieldname.Java.from_string
|
|
(container_write_string ^ (Typ.Procname.get_method callee_pname)) in
|
|
let dummy_access_ap =
|
|
fst receiver_ap, (snd receiver_ap) @ [AccessPath.FieldAccess dummy_fieldname] in
|
|
AccessDomain.add_access
|
|
(Unprotected (Some 0))
|
|
(make_access dummy_access_ap Write callee_loc)
|
|
AccessDomain.empty in
|
|
Some (true, false, false, callee_accesses, AttributeSetDomain.empty)
|
|
|
|
let get_summary caller_pdesc callee_pname actuals callee_loc tenv =
|
|
if is_container_write callee_pname tenv
|
|
then
|
|
let receiver_ap = match List.hd actuals with
|
|
| Some (HilExp.AccessPath receiver_ap) -> receiver_ap
|
|
| _ ->
|
|
failwithf
|
|
"Call to %a is marked as a container write, but has no receiver"
|
|
Typ.Procname.pp callee_pname in
|
|
make_container_write callee_pname receiver_ap callee_loc tenv
|
|
else
|
|
Summary.read_summary caller_pdesc callee_pname
|
|
|
|
(* return true if the given procname boxes a primitive type into a reference type *)
|
|
let is_box = function
|
|
| Typ.Procname.Java java_pname ->
|
|
begin
|
|
match Typ.Procname.java_get_class_name java_pname,
|
|
Typ.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
|
|
|
|
let add_reads exps loc accesses locks threads attribute_map proc_data =
|
|
List.fold
|
|
~f:(fun acc exp -> add_access exp loc Read acc locks threads attribute_map proc_data)
|
|
exps
|
|
~init:accesses
|
|
|
|
let exec_instr
|
|
(astate : Domain.astate)
|
|
({ ProcData.extras; tenv; pdesc; } as proc_data)
|
|
_
|
|
(instr : HilInstr.t) =
|
|
let open Domain in
|
|
match instr with
|
|
| Call (Some ret_base, Direct procname, actuals, _, loc)
|
|
when acquires_ownership procname tenv ->
|
|
let accesses =
|
|
add_reads actuals loc astate.accesses astate.locks
|
|
astate.threads astate.attribute_map proc_data in
|
|
let attribute_map =
|
|
AttributeMapDomain.add_attribute
|
|
(ret_base, []) Attribute.unconditionally_owned astate.attribute_map in
|
|
{ astate with accesses; attribute_map; }
|
|
|
|
| Call (ret_opt, Direct callee_pname, actuals, call_flags, loc) ->
|
|
let accesses =
|
|
add_reads actuals loc astate.accesses astate.locks
|
|
astate.threads astate.attribute_map proc_data in
|
|
let astate = { astate with accesses; } in
|
|
let astate =
|
|
match get_thread_model callee_pname with
|
|
| Threaded ->
|
|
{ astate with threads = true; }
|
|
| ThreadedIfTrue ->
|
|
begin
|
|
match ret_opt with
|
|
| Some ret_access_path ->
|
|
let attribute_map =
|
|
AttributeMapDomain.add_attribute
|
|
(ret_access_path, [])
|
|
(Choice Choice.OnMainThread)
|
|
astate.attribute_map in
|
|
{ astate with attribute_map; }
|
|
| None ->
|
|
failwithf
|
|
"Procedure %a specified as returning boolean, but returns nothing"
|
|
Typ.Procname.pp callee_pname
|
|
end
|
|
| Unknown -> 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 = true; }
|
|
else
|
|
match get_lock_model callee_pname with
|
|
| Lock ->
|
|
{ astate with locks = true; }
|
|
| Unlock ->
|
|
{ astate with locks = false; }
|
|
| LockedIfTrue ->
|
|
begin
|
|
match ret_opt with
|
|
| Some ret_access_path ->
|
|
let attribute_map =
|
|
AttributeMapDomain.add_attribute
|
|
(ret_access_path, [])
|
|
(Choice Choice.LockHeld)
|
|
astate.attribute_map in
|
|
{ astate with attribute_map; }
|
|
| None ->
|
|
failwithf
|
|
"Procedure %a specified as returning boolean, but returns nothing"
|
|
Typ.Procname.pp callee_pname
|
|
end
|
|
| NoEffect ->
|
|
match get_summary pdesc callee_pname actuals loc tenv with
|
|
| Some (callee_thumbs_up, callee_threads, callee_locks,
|
|
callee_accesses, return_attributes) ->
|
|
let update_caller_accesses pre callee_accesses caller_accesses =
|
|
let combined_accesses =
|
|
PathDomain.with_callsite callee_accesses (CallSite.make callee_pname loc)
|
|
|> PathDomain.join (AccessDomain.get_accesses pre caller_accesses) in
|
|
AccessDomain.add pre combined_accesses caller_accesses in
|
|
let thumbs_up = callee_thumbs_up && astate.thumbs_up in
|
|
let locks = callee_locks || astate.locks in
|
|
let threads = callee_threads || astate.threads in
|
|
let unprotected = is_unprotected locks threads pdesc in
|
|
(* add [ownership_accesses] to the [accesses_acc] with a protected pre if
|
|
[exp] is owned, and an appropriate unprotected pre otherwise *)
|
|
let add_ownership_access ownership_accesses actual_exp accesses_acc =
|
|
match actual_exp with
|
|
| HilExp.Constant _ ->
|
|
(* the actual is a constant, so it's owned in the caller. *)
|
|
accesses_acc
|
|
| HilExp.AccessPath 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 accesses for that actual, since they're all safe *)
|
|
accesses_acc
|
|
else
|
|
let pre =
|
|
if unprotected
|
|
then
|
|
let base = fst actual_access_path in
|
|
match FormalMap.get_formal_index base extras with
|
|
| Some formal_index ->
|
|
(* the actual passed to the current callee is rooted in a
|
|
formal *)
|
|
AccessPrecondition.Unprotected (Some formal_index)
|
|
| None ->
|
|
match
|
|
AttributeMapDomain.get_conditional_ownership_index
|
|
actual_access_path
|
|
astate.attribute_map
|
|
with
|
|
| Some formal_index ->
|
|
(* access path conditionally owned if [formal_index] is
|
|
owned *)
|
|
AccessPrecondition.Unprotected (Some formal_index)
|
|
| None ->
|
|
(* access path not rooted in a formal and not
|
|
conditionally owned *)
|
|
AccessPrecondition.unprotected
|
|
else
|
|
(* access protected by held lock *)
|
|
AccessPrecondition.Protected (make_excluder true threads) in
|
|
update_caller_accesses pre ownership_accesses accesses_acc
|
|
| _ ->
|
|
(* couldn't find access path, don't know if it's owned *)
|
|
update_caller_accesses
|
|
AccessPrecondition.unprotected ownership_accesses accesses_acc in
|
|
let accesses =
|
|
let update_accesses pre callee_accesses accesses_acc = match pre with
|
|
| AccessPrecondition.Protected _ ->
|
|
update_caller_accesses pre callee_accesses accesses_acc
|
|
| AccessPrecondition.Unprotected None ->
|
|
let pre' =
|
|
if unprotected
|
|
then pre
|
|
else AccessPrecondition.Protected (make_excluder true threads) in
|
|
update_caller_accesses pre' callee_accesses accesses_acc
|
|
| AccessPrecondition.Unprotected (Some index) ->
|
|
add_ownership_access
|
|
callee_accesses (List.nth_exn actuals index) accesses_acc in
|
|
AccessDomain.fold update_accesses callee_accesses astate.accesses in
|
|
let attribute_map =
|
|
propagate_return_attributes
|
|
ret_opt
|
|
return_attributes
|
|
actuals
|
|
astate.attribute_map
|
|
extras in
|
|
{ thumbs_up; locks; threads; accesses; attribute_map; }
|
|
| None ->
|
|
let should_assume_returns_ownership (call_flags : CallFlags.t) actuals =
|
|
(* assume non-interface methods with no summary and no parameters return
|
|
ownership *)
|
|
not (call_flags.cf_interface) && List.is_empty actuals in
|
|
if is_box callee_pname
|
|
then
|
|
match ret_opt, actuals with
|
|
| Some ret, HilExp.AccessPath actual_ap :: _
|
|
when AttributeMapDomain.has_attribute
|
|
actual_ap Functional astate.attribute_map ->
|
|
(* TODO: check for constants, which are functional? *)
|
|
let attribute_map =
|
|
AttributeMapDomain.add_attribute
|
|
(ret, [])
|
|
Functional
|
|
astate.attribute_map in
|
|
{ astate with attribute_map; }
|
|
| _ ->
|
|
astate
|
|
else if should_assume_returns_ownership call_flags actuals
|
|
then
|
|
match ret_opt with
|
|
| Some ret ->
|
|
let attribute_map =
|
|
AttributeMapDomain.add_attribute
|
|
(ret, [])
|
|
Attribute.unconditionally_owned
|
|
astate.attribute_map in
|
|
{ astate with attribute_map; }
|
|
| None ->
|
|
astate
|
|
else
|
|
astate in
|
|
begin
|
|
match ret_opt with
|
|
| Some (_, { Typ.desc=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 ->
|
|
let add_if_annotated predicate attribute attribute_map =
|
|
if PatternMatch.override_exists predicate tenv callee_pname
|
|
then
|
|
AttributeMapDomain.add_attribute (ret, []) 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
|
|
|
|
| Assign (lhs_access_path, rhs_exp, loc) ->
|
|
let rhs_accesses =
|
|
add_access
|
|
rhs_exp loc Read astate.accesses astate.locks
|
|
astate.threads astate.attribute_map proc_data in
|
|
let rhs_access_paths = HilExp.get_access_paths rhs_exp in
|
|
let is_functional =
|
|
not (List.is_empty rhs_access_paths) &&
|
|
List.for_all
|
|
~f:(fun access_path ->
|
|
AttributeMapDomain.has_attribute access_path Functional astate.attribute_map)
|
|
rhs_access_paths 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
|
|
(AccessPath lhs_access_path)
|
|
loc
|
|
Write
|
|
rhs_accesses
|
|
astate.locks
|
|
astate.threads
|
|
astate.attribute_map
|
|
proc_data in
|
|
let attribute_map =
|
|
propagate_attributes lhs_access_path rhs_exp astate.attribute_map extras in
|
|
{ astate with accesses; attribute_map; }
|
|
|
|
| Assume (assume_exp, _, _, loc) ->
|
|
let rec eval_binop op var e1 e2 =
|
|
match eval_bexp var e1, eval_bexp var e2 with
|
|
| Some b1, Some b2 -> Some (op b1 b2)
|
|
| _ -> None
|
|
(* return Some bool_value if the given boolean expression evaluates to bool_value when
|
|
[var] is set to true. return None if it has free variables that stop us from
|
|
evaluating it *)
|
|
and eval_bexp var = function
|
|
| HilExp.AccessPath ap when AccessPath.Raw.equal ap var ->
|
|
Some true
|
|
| HilExp.Constant c ->
|
|
Some (not (Const.iszero_int_float c))
|
|
| HilExp.UnaryOperator (Unop.LNot, e, _) ->
|
|
let b_opt = eval_bexp var e in
|
|
Option.map ~f:not b_opt
|
|
| HilExp.BinaryOperator (Binop.LAnd, e1, e2) ->
|
|
eval_binop (&&) var e1 e2
|
|
| HilExp.BinaryOperator (Binop.LOr, e1, e2) ->
|
|
eval_binop (||) var e1 e2
|
|
| HilExp.BinaryOperator (Binop.Eq, e1, e2) ->
|
|
eval_binop Bool.equal var e1 e2
|
|
| HilExp.BinaryOperator (Binop.Ne, e1, e2) ->
|
|
eval_binop (<>) var e1 e2
|
|
| _ ->
|
|
(* non-boolean expression; can't evaluate it *)
|
|
None in
|
|
let add_choice bool_value acc = function
|
|
| Choice.LockHeld ->
|
|
let locks = bool_value in
|
|
{ acc with locks; }
|
|
| Choice.OnMainThread ->
|
|
let threads = bool_value in
|
|
{ acc with threads; } in
|
|
|
|
let accesses =
|
|
add_access
|
|
assume_exp loc Read astate.accesses astate.locks
|
|
astate.threads astate.attribute_map proc_data in
|
|
let astate' =
|
|
match HilExp.get_access_paths assume_exp with
|
|
| [access_path] ->
|
|
let choices = AttributeMapDomain.get_choices access_path astate.attribute_map in
|
|
begin
|
|
match eval_bexp access_path assume_exp with
|
|
| Some bool_value ->
|
|
(* prune (prune_exp) can only evaluate to true if the choice is [bool_value].
|
|
add the constraint that the the choice must be [bool_value] to the state *)
|
|
List.fold ~f:(add_choice bool_value) ~init:astate choices
|
|
| None ->
|
|
astate
|
|
end
|
|
| _ ->
|
|
astate in
|
|
{ astate' with accesses; }
|
|
| Call (_, Indirect _, _, _, _) ->
|
|
failwithf "Unexpected indirect call instruction %a" HilInstr.pp instr
|
|
end
|
|
|
|
module Analyzer = AbstractInterpreter.Make (ProcCfg.Normal) (LowerHil.Make(TransferFunctions))
|
|
|
|
(* 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 (Typ.Name.name typename))
|
|
class_name
|
|
|
|
let is_call_to_immutable_collection_method tenv = function
|
|
| Typ.Procname.Java java_pname ->
|
|
is_immutable_collection_class (Typ.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 threadsafe_annotations =
|
|
Annotations.thread_safe ::
|
|
(ThreadSafetyConfig.AnnotationAliases.of_json Config.threadsafe_aliases)
|
|
|
|
(* returns true if the annotation is @ThreadSafe, @ThreadSafe(enableChecks = true), or is defined
|
|
as an alias of @ThreadSafe in a .inferconfig file. *)
|
|
let is_thread_safe item_annot =
|
|
let f ((annot : Annot.t), _) =
|
|
List.exists
|
|
~f:(fun annot_string ->
|
|
Annotations.annot_ends_with annot annot_string ||
|
|
String.equal annot.class_name annot_string)
|
|
threadsafe_annotations &&
|
|
match annot.Annot.parameters with
|
|
| ["false"] -> false
|
|
| _ -> true in
|
|
List.exists ~f item_annot
|
|
|
|
(* returns true if the annotation is @ThreadSafe(enableChecks = false) *)
|
|
let is_assumed_thread_safe item_annot =
|
|
let f (annot, _) =
|
|
Annotations.annot_ends_with annot Annotations.thread_safe &&
|
|
match annot.Annot.parameters with
|
|
| ["false"] -> true
|
|
| _ -> false in
|
|
List.exists ~f item_annot
|
|
|
|
let pdesc_is_assumed_thread_safe pdesc tenv =
|
|
is_assumed_thread_safe (Annotations.pdesc_get_return_annot pdesc) ||
|
|
PatternMatch.check_current_class_attributes
|
|
is_assumed_thread_safe tenv (Procdesc.get_proc_name pdesc)
|
|
|
|
(* 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 (Typ.Procname.is_class_initializer pn) &&
|
|
not (FbThreadSafety.is_logging_method pn) &&
|
|
not (is_call_to_immutable_collection_method tenv pn) &&
|
|
not (pdesc_is_assumed_thread_safe pdesc tenv)
|
|
|
|
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
|
|
is_thread_safe)
|
|
tenv
|
|
(Procdesc.get_proc_name pdesc)
|
|
|
|
let empty_post =
|
|
let initial_thumbs_up = true
|
|
and initial_known_on_ui_thread = false
|
|
and has_lock = false
|
|
and return_attrs = ThreadSafetyDomain.AttributeSetDomain.empty in
|
|
(initial_thumbs_up, initial_known_on_ui_thread, has_lock, ThreadSafetyDomain.AccessDomain.empty, return_attrs)
|
|
|
|
let analyze_procedure { Callbacks.proc_desc; tenv; summary; } =
|
|
let is_initializer tenv proc_name =
|
|
Typ.Procname.is_constructor proc_name || FbThreadSafety.is_custom_init tenv proc_name in
|
|
let open ThreadSafetyDomain in
|
|
(* convert the abstract state to a summary by dropping the id map *)
|
|
if should_analyze_proc proc_desc tenv
|
|
then
|
|
begin
|
|
if not (Procdesc.did_preanalysis proc_desc) then Preanal.do_liveness proc_desc tenv;
|
|
let extras = FormalMap.make proc_desc in
|
|
let proc_data = ProcData.make proc_desc tenv extras in
|
|
let initial =
|
|
let threads = runs_on_ui_thread proc_desc || is_thread_confined_method tenv proc_desc in
|
|
if is_initializer tenv (Procdesc.get_proc_name proc_desc)
|
|
then
|
|
let add_owned_formal acc formal_index =
|
|
match FormalMap.get_formal_base formal_index extras with
|
|
| Some base ->
|
|
AttributeMapDomain.add_attribute (base, []) Attribute.unconditionally_owned acc
|
|
| None ->
|
|
acc in
|
|
let owned_formals =
|
|
(* if a constructer 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)
|
|
else [0] (* express that the constructor owns [this] *) in
|
|
let attribute_map =
|
|
List.fold
|
|
~f:add_owned_formal
|
|
owned_formals
|
|
~init:ThreadSafetyDomain.empty.attribute_map in
|
|
{ ThreadSafetyDomain.empty with attribute_map; threads; }, IdAccessPathMapDomain.empty
|
|
else
|
|
{ ThreadSafetyDomain.empty with threads; }, IdAccessPathMapDomain.empty in
|
|
|
|
match Analyzer.compute_post proc_data ~initial ~debug:false with
|
|
| Some ({ thumbs_up; threads; locks; accesses; attribute_map; }, _) ->
|
|
let return_var_ap =
|
|
AccessPath.of_pvar
|
|
(Pvar.get_ret_pvar (Procdesc.get_proc_name proc_desc))
|
|
(Procdesc.get_ret_type proc_desc) in
|
|
let return_attributes =
|
|
try AttributeMapDomain.find return_var_ap attribute_map
|
|
with Not_found -> AttributeSetDomain.empty in
|
|
(* A hack for modeling lock_guard by releasing a
|
|
lock at the end of the procedure, as destructors are not modeled yet *)
|
|
let update_locks = match Procdesc.get_proc_name proc_desc with
|
|
| ObjC_Cpp _ when locks ->
|
|
let matcher = QualifiedCppName.Match.of_fuzzy_qual_names ["std::lock_guard"] in
|
|
(* Unlock, if the procedure contains a local field of type std::lock_guard *)
|
|
not (List.exists (Procdesc.get_locals proc_desc) ~f:(fun (_, ft) ->
|
|
Option.exists (Typ.name ft) ~f:(fun name ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Name.qual_name name))
|
|
))
|
|
| _ -> locks in
|
|
let post = thumbs_up, threads, update_locks, accesses, return_attributes in
|
|
Summary.update_summary post summary
|
|
| None ->
|
|
summary
|
|
end
|
|
else
|
|
Summary.update_summary empty_post summary
|
|
|
|
(* we assume two access paths can alias if their access parts are equal (we ignore the base). *)
|
|
let can_alias access_path1 access_path2 =
|
|
List.compare AccessPath.compare_access (snd access_path1) (snd access_path2)
|
|
|
|
module AccessListMap = Caml.Map.Make(struct
|
|
type t = AccessPath.Raw.t [@@deriving compare]
|
|
let compare = can_alias
|
|
end)
|
|
|
|
let get_current_class_and_threadsafe_superclasses tenv pname =
|
|
match pname with
|
|
| Typ.Procname.Java java_pname ->
|
|
let current_class = Typ.Procname.java_get_class_type_name java_pname in
|
|
let thread_safe_annotated_classes =
|
|
PatternMatch.find_superclasses_with_attributes
|
|
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:Typ.Name.equal thread_safe_annotated_classes current_class) then
|
|
match thread_safe_annotated_classes with
|
|
| hd::_ ->
|
|
F.asprintf "@\n Note: Superclass %a is marked %a."
|
|
(MF.wrap_monospaced Typ.Name.pp) hd
|
|
MF.pp_monospaced "@ThreadSafe"
|
|
| [] -> ""
|
|
else ""
|
|
| _ -> ""
|
|
|
|
let filter_by_access access_filter trace =
|
|
let open ThreadSafetyDomain in
|
|
PathDomain.Sinks.filter access_filter (PathDomain.sinks trace)
|
|
|> PathDomain.update_sinks trace
|
|
|
|
let get_all_accesses_with_pre pre_filter access_filter accesses =
|
|
let open ThreadSafetyDomain in
|
|
AccessDomain.fold
|
|
(fun pre trace acc ->
|
|
if pre_filter pre
|
|
then PathDomain.join (filter_by_access access_filter trace) acc
|
|
else acc)
|
|
accesses
|
|
PathDomain.empty
|
|
|
|
let get_all_accesses = get_all_accesses_with_pre (fun _ -> true)
|
|
|
|
let pp_container_access fmt (container_name, function_name) =
|
|
F.fprintf
|
|
fmt
|
|
"container %s via call to %s"
|
|
(MF.monospaced_to_string container_name)
|
|
(MF.monospaced_to_string function_name)
|
|
|
|
let pp_access fmt sink =
|
|
match get_container_write_desc sink with
|
|
| Some container_write_desc ->
|
|
pp_container_access fmt container_write_desc
|
|
| None ->
|
|
let access_path, _ = ThreadSafetyDomain.PathDomain.Sink.kind sink in
|
|
F.fprintf fmt "%a" (MF.wrap_monospaced AccessPath.pp_access_list) (snd access_path)
|
|
|
|
let desc_of_sink sink =
|
|
match get_container_write_desc sink with
|
|
| Some container_write_desc ->
|
|
F.asprintf "%a" pp_container_access container_write_desc
|
|
| None ->
|
|
let sink_pname = CallSite.pname (ThreadSafetyDomain.PathDomain.Sink.call_site sink) in
|
|
if Typ.Procname.equal sink_pname Typ.Procname.empty_block
|
|
then F.asprintf "access to %a" pp_access sink
|
|
else F.asprintf "call to %a" Typ.Procname.pp sink_pname
|
|
|
|
let trace_of_pname orig_sink orig_pdesc callee_pname =
|
|
let open ThreadSafetyDomain in
|
|
let orig_access = PathDomain.Sink.kind orig_sink in
|
|
match Summary.read_summary orig_pdesc callee_pname with
|
|
| Some (_, _, _, access_map, _) ->
|
|
get_all_accesses
|
|
(fun access ->
|
|
Int.equal (Access.compare (PathDomain.Sink.kind access) orig_access) 0)
|
|
access_map
|
|
| _ ->
|
|
PathDomain.empty
|
|
|
|
let make_trace_with_conflicts conflicts original_path pdesc =
|
|
let open ThreadSafetyDomain in
|
|
let loc_trace_of_path path =
|
|
PathDomain.to_sink_loc_trace ~desc_of_sink path in
|
|
let make_trace_for_sink sink =
|
|
let trace_of_pname = trace_of_pname sink pdesc in
|
|
match PathDomain.get_reportable_sink_path sink ~trace_of_pname with
|
|
| Some path -> loc_trace_of_path path
|
|
| None -> [] in
|
|
|
|
let original_trace = loc_trace_of_path original_path in
|
|
match conflicts with
|
|
| conflict_sink :: _ ->
|
|
(* create a trace for one of the conflicts and append it to the trace for the original sink *)
|
|
let conflict_trace = make_trace_for_sink conflict_sink in
|
|
let get_start_loc = function
|
|
| head :: _ -> head.Errlog.lt_loc
|
|
| [] -> Location.dummy in
|
|
let first_trace_spacer =
|
|
Errlog.make_trace_element
|
|
0 (get_start_loc original_trace) "<Beginning of read trace>" [] in
|
|
let second_trace_spacer =
|
|
Errlog.make_trace_element
|
|
0 (get_start_loc conflict_trace) "<Beginning of write trace>" [] in
|
|
(first_trace_spacer :: original_trace) @ (second_trace_spacer :: conflict_trace)
|
|
| [] ->
|
|
original_trace
|
|
|
|
let report_thread_safety_violation tenv pdesc ~make_description ~conflicts access =
|
|
let open ThreadSafetyDomain in
|
|
let pname = Procdesc.get_proc_name pdesc 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 loc = CallSite.loc initial_sink_site in
|
|
let ltr = make_trace_with_conflicts conflicts path pdesc in
|
|
let msg = Localise.to_issue_id Localise.thread_safety_violation in
|
|
let description = make_description tenv pname final_sink_site initial_sink_site final_sink in
|
|
let exn = Exceptions.Checkers (msg, Localise.verbatim_desc description) in
|
|
Reporting.log_error_deprecated ~store_summary:true pname ~loc ~ltr exn in
|
|
|
|
let trace_of_pname = trace_of_pname access pdesc in
|
|
Option.iter ~f:report_one_path (PathDomain.get_reportable_sink_path access ~trace_of_pname)
|
|
|
|
let pp_procname_short fmt = function
|
|
| Typ.Procname.Java java ->
|
|
F.fprintf fmt "%s.%s"
|
|
(Typ.Procname.java_get_class_name java) (Typ.Procname.java_get_method java)
|
|
| pname ->
|
|
Typ.Procname.pp fmt pname
|
|
|
|
let make_unprotected_write_description tenv pname final_sink_site initial_sink_site final_sink =
|
|
Format.asprintf
|
|
"Unprotected write. Non-private method %a%s %s %a outside of synchronization.%s"
|
|
(MF.wrap_monospaced pp_procname_short) pname
|
|
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
|
|
(if is_container_write_sink final_sink then "mutates" else "writes to field")
|
|
pp_access final_sink
|
|
(calculate_addendum_message tenv pname)
|
|
|
|
let make_read_write_race_description
|
|
conflicts tenv pname final_sink_site initial_sink_site final_sink =
|
|
let race_with_main_thread = List.exists
|
|
~f:(fun (_, _, threaded, _, _) -> threaded)
|
|
conflicts in
|
|
let conflicting_proc_names =
|
|
List.map
|
|
~f:(fun (_, _, _, _, pdesc) -> Procdesc.get_proc_name pdesc)
|
|
conflicts
|
|
|> Typ.Procname.Set.of_list in
|
|
let pp_conflicts fmt conflicts =
|
|
if Int.equal (Typ.Procname.Set.cardinal conflicts) 1
|
|
then Typ.Procname.pp fmt (Typ.Procname.Set.choose conflicts)
|
|
else Typ.Procname.Set.pp fmt conflicts in
|
|
let conflicts_description =
|
|
Format.asprintf "Potentially races with writes in method%s %a. %s"
|
|
(if Typ.Procname.Set.cardinal conflicting_proc_names > 1 then "s" else "")
|
|
(MF.wrap_monospaced pp_conflicts) conflicting_proc_names
|
|
(if race_with_main_thread then
|
|
"\n Note: some of these write conflicts are confined to the UI or another thread, \
|
|
but the current method is not specified to be. Consider adding synchronization \
|
|
or a @ThreadConfined annotation to the current method."
|
|
else "") in
|
|
Format.asprintf "Read/Write race. Non-private method %a%s reads from field %a. %s %s"
|
|
(MF.wrap_monospaced pp_procname_short) pname
|
|
(if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly")
|
|
pp_access final_sink
|
|
conflicts_description
|
|
(calculate_addendum_message tenv pname)
|
|
|
|
(** 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 : Typ.Procname.Set.t;
|
|
reported_reads : Typ.Procname.Set.t;
|
|
}
|
|
|
|
let empty_reported =
|
|
let reported_sites = CallSite.Set.empty in
|
|
let reported_writes = Typ.Procname.Set.empty in
|
|
let reported_reads = Typ.Procname.Set.empty in
|
|
{ reported_sites; reported_reads; reported_writes; }
|
|
|
|
(* return true if procedure is at an abstraction boundary or reporting has been explicitly
|
|
requested via @ThreadSafe *)
|
|
let should_report_on_proc proc_desc tenv =
|
|
let proc_name = Procdesc.get_proc_name proc_desc in
|
|
is_thread_safe_method proc_desc tenv ||
|
|
(not (Typ.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))
|
|
|
|
(** 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 aggregated_access_map =
|
|
let open ThreadSafetyDomain in
|
|
let is_duplicate_report access pname { reported_sites; reported_writes; reported_reads; } =
|
|
CallSite.Set.mem (TraceElem.call_site access) reported_sites ||
|
|
Typ.Procname.Set.mem
|
|
pname
|
|
(match snd (TraceElem.kind access) with
|
|
| Access.Write -> reported_writes
|
|
| Access.Read -> reported_reads) in
|
|
let update_reported access pname reported =
|
|
let reported_sites = CallSite.Set.add (TraceElem.call_site access) reported.reported_sites in
|
|
match snd (TraceElem.kind access) with
|
|
| Access.Write ->
|
|
let reported_writes = Typ.Procname.Set.add pname reported.reported_writes in
|
|
{ reported with reported_writes; reported_sites; }
|
|
| Access.Read ->
|
|
let reported_reads = Typ.Procname.Set.add pname reported.reported_reads in
|
|
{ reported with reported_reads; reported_sites; } in
|
|
let report_unsafe_access (access, pre, threaded, tenv, pdesc) accesses reported_acc =
|
|
let pname = Procdesc.get_proc_name pdesc in
|
|
if is_duplicate_report access pname reported_acc
|
|
then
|
|
reported_acc
|
|
else
|
|
match snd (TraceElem.kind access), pre with
|
|
| Access.Write, AccessPrecondition.Unprotected _ ->
|
|
begin
|
|
match Procdesc.get_proc_name pdesc with
|
|
| Java _ ->
|
|
if threaded
|
|
then
|
|
reported_acc
|
|
else
|
|
begin
|
|
(* unprotected write. warn. *)
|
|
report_thread_safety_violation
|
|
tenv
|
|
pdesc
|
|
~make_description:make_unprotected_write_description
|
|
~conflicts:[]
|
|
access;
|
|
update_reported access pname reported_acc
|
|
end
|
|
| _ ->
|
|
(* Do not report unprotected writes for ObjC_Cpp *)
|
|
reported_acc
|
|
end
|
|
| Access.Write, AccessPrecondition.Protected _ ->
|
|
(* protected write, do nothing *)
|
|
reported_acc
|
|
| Access.Read, AccessPrecondition.Unprotected _ ->
|
|
(* unprotected read. report all writes as conflicts for java *)
|
|
(* for c++ filter out unprotected writes *)
|
|
let is_cpp_protected_write pre = match pre with
|
|
| AccessPrecondition.Unprotected _ -> Typ.Procname.is_java pname
|
|
| AccessPrecondition.Protected _ -> true in
|
|
let all_writes =
|
|
List.filter
|
|
~f:(fun (other_access, pre, other_threaded, _, _) ->
|
|
TraceElem.is_write other_access && not (threaded && other_threaded)
|
|
&& is_cpp_protected_write pre)
|
|
accesses in
|
|
if List.is_empty all_writes
|
|
then
|
|
reported_acc
|
|
else
|
|
begin
|
|
report_thread_safety_violation
|
|
tenv
|
|
pdesc
|
|
~make_description:(make_read_write_race_description all_writes)
|
|
~conflicts:(List.map ~f:(fun (access, _, _, _, _) -> access) all_writes)
|
|
access;
|
|
update_reported access pname reported_acc
|
|
end
|
|
| Access.Read, AccessPrecondition.Protected excl ->
|
|
(* protected read.
|
|
report unprotected writes and opposite protected writes as conflicts
|
|
Thread and Lock are opposites of one another, and
|
|
Both has no opposite*)
|
|
let is_opposite = function
|
|
| Excluder.Lock , Excluder.Thread
|
|
-> true
|
|
| Excluder.Thread , Excluder.Lock
|
|
-> true
|
|
| _ , _ -> false in
|
|
let conflicting_writes =
|
|
List.filter
|
|
~f:(fun (access, pre, _, _, _) ->
|
|
match pre with
|
|
| AccessPrecondition.Unprotected _ ->
|
|
TraceElem.is_write access
|
|
| AccessPrecondition.Protected other_excl
|
|
when is_opposite (excl , other_excl) ->
|
|
TraceElem.is_write access
|
|
| _ ->
|
|
false)
|
|
accesses in
|
|
if List.is_empty conflicting_writes
|
|
then
|
|
reported_acc
|
|
else
|
|
begin
|
|
(* protected read with conflicting unprotected write(s). warn. *)
|
|
report_thread_safety_violation
|
|
tenv
|
|
pdesc
|
|
~make_description:(make_read_write_race_description conflicting_writes)
|
|
~conflicts:(List.map ~f:(fun (access, _, _, _, _) -> access) conflicting_writes)
|
|
access;
|
|
update_reported access pname reported_acc
|
|
end in
|
|
AccessListMap.fold
|
|
(fun _ grouped_accesses reported_acc ->
|
|
(* reset the reported reads and writes for each memory location *)
|
|
let reported =
|
|
{ reported_acc with reported_writes = Typ.Procname.Set.empty;
|
|
reported_reads = Typ.Procname.Set.empty; } in
|
|
let accessed_by_threadsafe_method =
|
|
List.exists
|
|
~f:(fun (_, _, _, tenv, pdesc) -> is_thread_safe_method pdesc tenv)
|
|
grouped_accesses in
|
|
let class_has_mutex_member objc_cpp tenv =
|
|
let class_name = Typ.Procname.objc_cpp_get_class_type_name objc_cpp in
|
|
let matcher = QualifiedCppName.Match.of_fuzzy_qual_names ["std::mutex"] in
|
|
Option.exists (Tenv.lookup tenv class_name) ~f:(fun class_str ->
|
|
(* check if the class contains a member of type std::mutex *)
|
|
List.exists (class_str.Typ.Struct.fields) ~f:(fun (_, ft, _) ->
|
|
Option.exists (Typ.name ft) ~f:(fun name ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Name.qual_name name))
|
|
)
|
|
) in
|
|
let should_report pdesc tenv =
|
|
match Procdesc.get_proc_name pdesc with
|
|
| Java _ as pname ->
|
|
(* report if
|
|
- the method/class of the access is thread-safe
|
|
(or an override or superclass is), or
|
|
- any access is in a field marked thread-safe (or an override) *)
|
|
(accessed_by_threadsafe_method
|
|
||
|
|
let is_class_threadsafe =
|
|
not
|
|
(let current_class_marked_not_threadsafe =
|
|
PatternMatch.check_current_class_attributes
|
|
Annotations.ia_is_not_thread_safe tenv pname
|
|
in
|
|
current_class_marked_not_threadsafe)
|
|
&&
|
|
(let current_class_or_super_marked_threadsafe =
|
|
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
|
|
current_class_or_super_marked_threadsafe)
|
|
in
|
|
is_class_threadsafe)
|
|
&& should_report_on_proc pdesc tenv
|
|
| ObjC_Cpp objc_cpp ->
|
|
(* report if the class has a mutex member *)
|
|
class_has_mutex_member objc_cpp tenv
|
|
| _ ->
|
|
false
|
|
in
|
|
let reportable_accesses =
|
|
List.filter ~f:(fun (_, _, _, tenv, pdesc) -> should_report pdesc tenv) grouped_accesses in
|
|
List.fold
|
|
~f:(fun acc access -> report_unsafe_access access reportable_accesses acc)
|
|
reportable_accesses
|
|
~init:reported)
|
|
aggregated_access_map
|
|
empty_reported
|
|
|> ignore
|
|
|
|
(* equivalence relation computing whether two access paths may refer to the
|
|
same heap location. *)
|
|
let may_alias tenv p1 p2 =
|
|
let open Typ in
|
|
let open AccessPath in
|
|
phys_equal p1 p2 ||
|
|
match List.last_exn (snd p1), List.last_exn (snd p2) with
|
|
| FieldAccess _, ArrayAccess _ | ArrayAccess _, FieldAccess _ -> false
|
|
(* fields in Java contain the class name /declaring/ them
|
|
thus two fields can be aliases *iff* they are equal *)
|
|
| FieldAccess f1, FieldAccess f2 -> Fieldname.equal f1 f2
|
|
(* if arrays of objects that have an inheritance rel then they can alias *)
|
|
| ArrayAccess {desc=Tptr ({desc=Tstruct tn1}, _)},
|
|
ArrayAccess {desc=Tptr ({desc=Tstruct tn2}, _)} ->
|
|
PatternMatch.is_subtype tenv tn1 tn2 ||
|
|
PatternMatch.is_subtype tenv tn2 tn1
|
|
(* primitive type arrays can alias if the prim. type is the same *)
|
|
| ArrayAccess t1, ArrayAccess t2 ->
|
|
equal_desc t1.desc t2.desc
|
|
|
|
(* take a results table and quotient it by the may_alias relation *)
|
|
let quotient_access_map acc_map =
|
|
let rec aux acc m =
|
|
if AccessListMap.is_empty m then
|
|
acc
|
|
else
|
|
let k, vals = AccessListMap.choose m in
|
|
let (_, _, _, tenv, _) =
|
|
List.find_exn vals
|
|
~f:(fun (elem, _, _, _, _) ->
|
|
AccessPath.Raw.equal k (ThreadSafetyDomain.TraceElem.kind elem |> fst))
|
|
in
|
|
(* assumption: the tenv for k is sufficient for k' too *)
|
|
let (k_part, non_k_part) =
|
|
AccessListMap.partition (fun k' _ -> may_alias tenv k k') m in
|
|
if AccessListMap.is_empty k_part
|
|
then failwith "may_alias is not reflexive!";
|
|
let k_accesses =
|
|
AccessListMap.fold
|
|
(fun _ v acc' -> List.append v acc')
|
|
k_part
|
|
[]
|
|
in
|
|
let new_acc = AccessListMap.add k k_accesses acc in
|
|
aux new_acc non_k_part
|
|
in
|
|
aux AccessListMap.empty acc_map
|
|
|
|
(* decide if we should throw away a path 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 (_, path) =
|
|
let check_access_step = function
|
|
| AccessPath.ArrayAccess _ -> false
|
|
| AccessPath.FieldAccess fld ->
|
|
String.is_substring ~substring:"$SwitchMap" (Fieldname.to_string fld) in
|
|
List.exists path ~f:check_access_step
|
|
|
|
(* create a map from [abstraction of a memory loc] -> accesses that may touch that memory loc. for
|
|
now, our abstraction 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 file_env =
|
|
let aggregate_post (_ , threaded, _, accesses, _) tenv pdesc acc =
|
|
let open ThreadSafetyDomain in
|
|
AccessDomain.fold
|
|
(fun pre accesses acc ->
|
|
PathDomain.Sinks.fold
|
|
(fun access acc ->
|
|
let access_path, _ = TraceElem.kind access in
|
|
if should_filter_access access_path then acc else
|
|
let grouped_accesses =
|
|
try AccessListMap.find access_path acc
|
|
with Not_found -> [] in
|
|
AccessListMap.add
|
|
access_path
|
|
((access, pre, threaded, tenv, pdesc) :: grouped_accesses)
|
|
acc)
|
|
(PathDomain.sinks accesses)
|
|
acc)
|
|
accesses
|
|
acc in
|
|
let aggregate_posts acc (_, tenv, proc_name, proc_desc) =
|
|
match Summary.read_summary proc_desc proc_name with
|
|
| Some summary -> aggregate_post summary tenv proc_desc acc
|
|
| None -> acc in
|
|
List.fold ~f:aggregate_posts file_env ~init:AccessListMap.empty |> quotient_access_map
|
|
|
|
(* 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 _ _ _ file_env =
|
|
report_unsafe_accesses (make_results_table file_env)
|