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.
728 lines
28 KiB
728 lines
28 KiB
(*
|
|
* Copyright (c) 2017-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*)
|
|
|
|
open! IStd
|
|
module F = Format
|
|
module L = Logging
|
|
module MF = MarkupFormatter
|
|
|
|
module AnnotationAliases = struct
|
|
let of_json = function
|
|
| `List aliases ->
|
|
List.map ~f:Yojson.Basic.Util.to_string aliases
|
|
| _ ->
|
|
L.(die UserError)
|
|
"Couldn't parse thread-safety annotation aliases; expected list of strings"
|
|
end
|
|
|
|
module Models = struct
|
|
type lock = Lock | Unlock | LockedIfTrue | NoEffect
|
|
|
|
type thread = BackgroundThread | MainThread | MainThreadIfTrue | UnknownThread
|
|
|
|
type container_access = ContainerRead | ContainerWrite
|
|
|
|
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_thread = function
|
|
| Typ.Procname.Java java_pname when is_thread_utils_type java_pname -> (
|
|
match Typ.Procname.Java.get_method java_pname with
|
|
| "assertMainThread" | "assertOnUiThread" | "checkOnMainThread" ->
|
|
MainThread
|
|
| "isMainThread" | "isUiThread" ->
|
|
MainThreadIfTrue
|
|
| "assertOnBackgroundThread" | "assertOnNonUiThread" | "checkOnNonUiThread" ->
|
|
BackgroundThread
|
|
| _ ->
|
|
UnknownThread )
|
|
| _ ->
|
|
UnknownThread
|
|
|
|
|
|
let get_lock =
|
|
let is_cpp_lock =
|
|
let matcher_lock =
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
[ "apache::thrift::concurrency::ReadWriteMutex::acquireRead"
|
|
; "apache::thrift::concurrency::ReadWriteMutex::acquireWrite"
|
|
; "folly::MicroSpinLock::lock"
|
|
; "folly::RWSpinLock::lock"
|
|
; "folly::RWSpinLock::lock_shared"
|
|
; "folly::SharedMutexImpl::lockExclusiveImpl"
|
|
; "folly::SharedMutexImpl::lockSharedImpl"
|
|
; "std::mutex::lock"
|
|
; "std::unique_lock::lock"
|
|
; "std::lock" ]
|
|
in
|
|
let matcher_lock_constructor =
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
[ "std::lock_guard::lock_guard"
|
|
; "std::unique_lock::unique_lock"
|
|
; "folly::LockedPtr::LockedPtr" ]
|
|
in
|
|
fun pname actuals ->
|
|
QualifiedCppName.Match.match_qualifiers matcher_lock (Typ.Procname.get_qualifiers pname)
|
|
|| QualifiedCppName.Match.match_qualifiers matcher_lock_constructor
|
|
(Typ.Procname.get_qualifiers pname)
|
|
(* Passing additional parameter allows to defer the lock *)
|
|
&& Int.equal 2 (List.length actuals)
|
|
and is_cpp_unlock =
|
|
let matcher =
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
[ "apache::thrift::concurrency::ReadWriteMutex::release"
|
|
; "folly::MicroSpinLock::unlock"
|
|
; "folly::RWSpinLock::unlock"
|
|
; "folly::RWSpinLock::unlock_shared"
|
|
; "folly::SharedMutexImpl::unlock"
|
|
; "folly::SharedMutexImpl::unlock_shared"
|
|
; "std::lock_guard::~lock_guard"
|
|
; "std::mutex::unlock"
|
|
; "std::unique_lock::unlock"
|
|
; "std::unique_lock::~unique_lock"
|
|
; "folly::LockedPtr::~LockedPtr" ]
|
|
in
|
|
fun pname ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
and is_cpp_trylock =
|
|
let matcher =
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
["std::unique_lock::owns_lock"; "std::unique_lock::try_lock"]
|
|
in
|
|
fun pname ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
in
|
|
fun pname actuals ->
|
|
match pname with
|
|
| Typ.Procname.Java java_pname
|
|
-> (
|
|
if is_thread_utils_method "assertHoldsLock" (Typ.Procname.Java java_pname) then Lock
|
|
else
|
|
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 )
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_lock pname actuals ->
|
|
Lock
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_unlock pname ->
|
|
Unlock
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_trylock pname ->
|
|
LockedIfTrue
|
|
| 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_container_access =
|
|
let is_cpp_container_read =
|
|
let is_container_operator pname_qualifiers =
|
|
match QualifiedCppName.extract_last pname_qualifiers with
|
|
| Some (last, _) ->
|
|
String.equal last "operator[]"
|
|
| None ->
|
|
false
|
|
in
|
|
let matcher = QualifiedCppName.Match.of_fuzzy_qual_names ["std::map::find"] in
|
|
fun pname ->
|
|
let pname_qualifiers = Typ.Procname.get_qualifiers pname in
|
|
QualifiedCppName.Match.match_qualifiers matcher pname_qualifiers
|
|
|| is_container_operator pname_qualifiers
|
|
and is_cpp_container_write =
|
|
let matcher =
|
|
QualifiedCppName.Match.of_fuzzy_qual_names ["std::map::operator[]"; "std::map::erase"]
|
|
in
|
|
fun pname ->
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
in
|
|
fun 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 get_container_access_ 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" ) ) ->
|
|
Some ContainerWrite
|
|
| ( ("android.util.SparseArray" | "android.support.v4.util.SparseArrayCompat")
|
|
, ("clone" | "get" | "indexOfKey" | "indexOfValue" | "keyAt" | "size" | "valueAt") ) ->
|
|
Some ContainerRead
|
|
| ( "android.support.v4.util.SimpleArrayMap"
|
|
, ( "clear"
|
|
| "ensureCapacity"
|
|
| "put"
|
|
| "putAll"
|
|
| "remove"
|
|
| "removeAt"
|
|
| "setValueAt" ) ) ->
|
|
Some ContainerWrite
|
|
| ( "android.support.v4.util.SimpleArrayMap"
|
|
, ( "containsKey"
|
|
| "containsValue"
|
|
| "get"
|
|
| "hashCode"
|
|
| "indexOfKey"
|
|
| "isEmpty"
|
|
| "keyAt"
|
|
| "size"
|
|
| "valueAt" ) ) ->
|
|
Some ContainerRead
|
|
| "android.support.v4.util.Pools$SimplePool", ("acquire" | "release") ->
|
|
Some ContainerWrite
|
|
| "java.util.List", ("add" | "addAll" | "clear" | "remove" | "set") ->
|
|
Some ContainerWrite
|
|
| ( "java.util.List"
|
|
, ( "contains"
|
|
| "containsAll"
|
|
| "equals"
|
|
| "get"
|
|
| "hashCode"
|
|
| "indexOf"
|
|
| "isEmpty"
|
|
| "iterator"
|
|
| "lastIndexOf"
|
|
| "listIterator"
|
|
| "size"
|
|
| "toArray" ) ) ->
|
|
Some ContainerRead
|
|
| "java.util.Map", ("clear" | "put" | "putAll" | "remove") ->
|
|
Some ContainerWrite
|
|
| ( "java.util.Map"
|
|
, ( "containsKey"
|
|
| "containsValue"
|
|
| "entrySet"
|
|
| "equals"
|
|
| "get"
|
|
| "hashCode"
|
|
| "isEmpty"
|
|
| "keySet"
|
|
| "size"
|
|
| "values" ) ) ->
|
|
Some ContainerRead
|
|
| _ ->
|
|
None
|
|
in
|
|
PatternMatch.supertype_find_map_opt tenv get_container_access_ typename
|
|
(* The following order matters: we want to check if pname is a container write
|
|
before we check if pname is a container read. This is due to a different
|
|
treatment between std::map::operator[] and all other operator[]. *)
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname
|
|
when is_cpp_container_write pname ->
|
|
Some ContainerWrite
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_container_read pname ->
|
|
Some ContainerRead
|
|
| _ ->
|
|
None
|
|
|
|
|
|
(** holds of procedure names which should not be analyzed in order to avoid known sources of
|
|
inaccuracy *)
|
|
let should_skip =
|
|
let matcher =
|
|
lazy
|
|
(QualifiedCppName.Match.of_fuzzy_qual_names ~prefix:true
|
|
[ "folly::AtomicStruct"
|
|
; "folly::fbstring_core"
|
|
; "folly::Future"
|
|
; "folly::futures"
|
|
; "folly::LockedPtr"
|
|
; "folly::Optional"
|
|
; "folly::Promise"
|
|
; "folly::ThreadLocal"
|
|
; "folly::detail::SingletonHolder"
|
|
; "std::atomic"
|
|
; "std::vector" ])
|
|
in
|
|
function
|
|
| Typ.Procname.ObjC_Cpp cpp_pname as pname ->
|
|
Typ.Procname.ObjC_Cpp.is_destructor cpp_pname
|
|
|| QualifiedCppName.Match.match_qualifiers (Lazy.force matcher)
|
|
(Typ.Procname.get_qualifiers pname)
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** return true if this function is library code from the JDK core libraries or Android *)
|
|
let is_java_library = function
|
|
| Typ.Procname.Java java_pname -> (
|
|
match Typ.Procname.Java.get_package java_pname with
|
|
| Some package_name ->
|
|
String.is_prefix ~prefix:"java." package_name
|
|
|| String.is_prefix ~prefix:"android." package_name
|
|
|| String.is_prefix ~prefix:"com.google." package_name
|
|
| None ->
|
|
false )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let is_builder_function = function
|
|
| Typ.Procname.Java java_pname ->
|
|
String.is_suffix ~suffix:"$Builder" (Typ.Procname.Java.get_class_name java_pname)
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let has_return_annot predicate pn =
|
|
Annotations.pname_has_return_annot pn ~attrs_of_pname:Summary.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 -> (
|
|
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 ~equal:String.equal non_functional_resource_methods method_name)
|
|
| _ ->
|
|
false )
|
|
| _ ->
|
|
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 -> (
|
|
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"
|
|
| "android.support.v4.util.Pools$SimplePool"
|
|
| "android.support.v4.util.Pools$SynchronizedPool" )
|
|
, "acquire" ) ->
|
|
(* a pool should own all of its objects *)
|
|
true
|
|
| _ ->
|
|
false )
|
|
| _ ->
|
|
false
|
|
in
|
|
is_allocation pname || is_owned_in_library pname
|
|
|| PatternMatch.override_exists is_owned_in_library tenv pname
|
|
|
|
|
|
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
|
|
|
|
|
|
(* return true if the given procname boxes a primitive type into a reference type *)
|
|
let is_box = function
|
|
| Typ.Procname.Java java_pname -> (
|
|
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 )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(* Methods in @ThreadConfined classes and methods annotated with @ThreadConfined 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)
|
|
|
|
|
|
let threadsafe_annotations =
|
|
Annotations.thread_safe :: 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
|
|
( match pn with
|
|
| Typ.Procname.Java java_pname ->
|
|
Typ.Procname.Java.is_class_initializer java_pname
|
|
|| Typ.Name.Java.is_external (Typ.Procname.Java.get_class_type_name java_pname)
|
|
(* third party code may be hard to change, not useful to report races there *)
|
|
| _ ->
|
|
false )
|
|
&& not (FbThreadSafety.is_logging_method pn) && not (pdesc_is_assumed_thread_safe pdesc tenv)
|
|
&& not (should_skip pn)
|
|
|
|
|
|
let get_current_class_and_annotated_superclasses is_annot tenv pname =
|
|
match pname with
|
|
| Typ.Procname.Java java_pname ->
|
|
let current_class = Typ.Procname.Java.get_class_type_name java_pname in
|
|
let annotated_classes =
|
|
PatternMatch.find_superclasses_with_attributes is_annot tenv current_class
|
|
in
|
|
Some (current_class, annotated_classes)
|
|
| _ ->
|
|
None
|
|
|
|
|
|
let find_annotated_or_overriden_annotated_method is_annot pname tenv =
|
|
PatternMatch.override_find
|
|
(fun pn ->
|
|
Annotations.pname_has_return_annot pn ~attrs_of_pname:Summary.proc_resolve_attributes
|
|
is_annot )
|
|
tenv pname
|
|
|
|
|
|
(* 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 tenv proc_desc =
|
|
(* assume that methods annotated with @UiThread, @OnEvent, @OnBind, @OnMount, @OnUnbind,
|
|
@OnUnmount always run on the UI thread *)
|
|
let is_annot 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
|
|
in
|
|
let pname = Procdesc.get_proc_name proc_desc in
|
|
if Annotations.pdesc_has_return_annot proc_desc is_annot then
|
|
Some
|
|
(F.asprintf "%a is annotated %s"
|
|
(MF.wrap_monospaced Typ.Procname.pp)
|
|
pname
|
|
(MF.monospaced_to_string Annotations.ui_thread))
|
|
else
|
|
match find_annotated_or_overriden_annotated_method is_annot pname tenv with
|
|
| Some override_pname ->
|
|
Some
|
|
(F.asprintf "class %a overrides %a, which is annotated %s"
|
|
(MF.wrap_monospaced Typ.Procname.pp)
|
|
pname
|
|
(MF.wrap_monospaced Typ.Procname.pp)
|
|
override_pname
|
|
(MF.monospaced_to_string Annotations.ui_thread))
|
|
| None ->
|
|
match
|
|
get_current_class_and_annotated_superclasses Annotations.ia_is_ui_thread tenv pname
|
|
with
|
|
| Some (current_class, _)
|
|
when let open PatternMatch in
|
|
is_subtype_of_str tenv current_class "android.app.Service"
|
|
&& not (is_subtype_of_str tenv current_class "android.app.IntentService") ->
|
|
Some
|
|
(F.asprintf "class %s extends %s"
|
|
(MF.monospaced_to_string (Typ.Name.name current_class))
|
|
(MF.monospaced_to_string "android.app.Service"))
|
|
| Some (current_class, super_class :: _) ->
|
|
Some
|
|
(F.asprintf "class %s extends %a, which is annotated %s"
|
|
(MF.monospaced_to_string (Typ.Name.name current_class))
|
|
(MF.wrap_monospaced Typ.Name.pp) super_class
|
|
(MF.monospaced_to_string Annotations.ui_thread))
|
|
| _ ->
|
|
None
|
|
|
|
|
|
let get_current_class_and_threadsafe_superclasses tenv pname =
|
|
get_current_class_and_annotated_superclasses is_thread_safe tenv pname
|
|
|
|
|
|
let is_thread_safe_class pname tenv =
|
|
not
|
|
((* current class not marked thread-safe *)
|
|
PatternMatch.check_current_class_attributes Annotations.ia_is_not_thread_safe tenv pname)
|
|
&&
|
|
(* current class or superclass is marked thread-safe *)
|
|
match get_current_class_and_threadsafe_superclasses tenv pname with
|
|
| Some (_, thread_safe_annotated_classes) ->
|
|
not (List.is_empty thread_safe_annotated_classes)
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let is_thread_safe_method pname tenv =
|
|
find_annotated_or_overriden_annotated_method is_thread_safe pname tenv |> Option.is_some
|
|
|
|
|
|
let is_marked_thread_safe pdesc tenv =
|
|
let pname = Procdesc.get_proc_name pdesc in
|
|
is_thread_safe_class pname tenv || is_thread_safe_method pname tenv
|
|
|
|
|
|
(* 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_name tenv
|
|
|| not
|
|
( match proc_name with
|
|
| Typ.Procname.Java java_pname ->
|
|
Typ.Procname.Java.is_autogen_method java_pname
|
|
| _ ->
|
|
false )
|
|
&& Procdesc.get_access proc_desc <> PredSymb.Private
|
|
&& not (Annotations.pdesc_return_annot_ends_with proc_desc Annotations.visibleForTesting)
|
|
|
|
|
|
let is_call_of_class_or_superclass ?(method_prefix= false) ?(actuals_pred= fun _ -> true)
|
|
class_names method_name tenv pn actuals =
|
|
actuals_pred actuals
|
|
&&
|
|
match pn with
|
|
| Typ.Procname.Java java_pname ->
|
|
let classname = Typ.Procname.Java.get_class_type_name java_pname in
|
|
let mthd = Typ.Procname.Java.get_method java_pname in
|
|
let is_target_class =
|
|
let targets = List.map class_names ~f:Typ.Name.Java.from_string in
|
|
fun tname _ -> List.mem targets tname ~equal:Typ.Name.equal
|
|
in
|
|
( if method_prefix then String.is_prefix mthd ~prefix:method_name
|
|
else String.equal mthd method_name )
|
|
&& PatternMatch.supertype_exists tenv is_target_class classname
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** magical value from https://developer.android.com/topic/performance/vitals/anr *)
|
|
let android_anr_time_limit = 5.0
|
|
|
|
(** can this timeout cause an anr? *)
|
|
let is_excessive_timeout =
|
|
let time_units =
|
|
String.Map.of_alist_exn
|
|
[ ("NANOSECONDS", 0.000_000_001)
|
|
; ("MICROSECONDS", 0.000_001)
|
|
; ("MILLISECONDS", 0.001)
|
|
; ("SECONDS", 1.0)
|
|
; ("MINUTES", 60.0)
|
|
; ("HOURS", 3_600.0)
|
|
; ("DAYS", 86_400.0) ]
|
|
in
|
|
fun duration_exp timeunit_exp ->
|
|
match (duration_exp, timeunit_exp) with
|
|
| HilExp.Constant (Const.Cint duration_lit), HilExp.AccessExpression timeunit_acc_exp -> (
|
|
match AccessExpression.to_access_path timeunit_acc_exp with
|
|
| _, [AccessPath.FieldAccess field]
|
|
when String.equal "java.util.concurrent.TimeUnit" (Typ.Fieldname.Java.get_class field) ->
|
|
let fieldname = Typ.Fieldname.Java.get_field field in
|
|
let duration = float_of_int (IntLit.to_int duration_lit) in
|
|
String.Map.find time_units fieldname
|
|
|> Option.value_map ~default:false ~f:(fun unit_in_secs ->
|
|
unit_in_secs *. duration >. android_anr_time_limit )
|
|
| _ ->
|
|
false )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** It's surprisingly difficult making any sense of the official documentation on java.io classes
|
|
as to whether methods may block. We approximate these by calls in traditionally blocking
|
|
classes (non-channel based I/O), to methods `(Reader|InputStream).read*`.
|
|
Initially, (Writer|OutputStream).(flush|close) were also matched, but this generated too
|
|
many reports *)
|
|
|
|
(* let is_blocking_java_io =
|
|
is_call_of_class_or_superclass ["java.io.Reader"; "java.io.InputStream"] ~method_prefix:true
|
|
"read" *)
|
|
|
|
let actuals_are_empty_or_timeout = function
|
|
| [_] ->
|
|
true
|
|
| [_; duration; timeunit] ->
|
|
is_excessive_timeout duration timeunit
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** is the method called CountDownLath.await or on subclass? *)
|
|
let is_countdownlatch_await =
|
|
is_call_of_class_or_superclass ~actuals_pred:actuals_are_empty_or_timeout
|
|
["java.util.concurrent.CountDownLatch"] "await"
|
|
|
|
|
|
(** an IBinder.transact call is an RPC. If the 4th argument (5th counting `this` as the first)
|
|
is int-zero then a reply is expected and returned from the remote process, thus potentially
|
|
blocking. If the 4th argument is anything else, we assume a one-way call which doesn't block.
|
|
*)
|
|
let is_two_way_binder_transact =
|
|
let actuals_pred actuals =
|
|
List.nth actuals 4 |> Option.value_map ~default:false ~f:HilExp.is_int_zero
|
|
in
|
|
is_call_of_class_or_superclass ~actuals_pred ["android.os.IBinder"] "transact"
|
|
|
|
|
|
(** is it a call to android.view.View.getWindowVisibleDisplayFrame or on sublass? *)
|
|
let is_getWindowVisibleDisplayFrame =
|
|
is_call_of_class_or_superclass ["android.view.View"] "getWindowVisibleDisplayFrame"
|
|
|
|
|
|
(** is it a call to Future.get() or on sublass? *)
|
|
let is_future_get =
|
|
is_call_of_class_or_superclass ~actuals_pred:actuals_are_empty_or_timeout
|
|
["java.util.concurrent.Future"] "get"
|
|
|
|
|
|
let is_accountManager_setUserData =
|
|
is_call_of_class_or_superclass ["android.accounts.AccountManager"] "setUserData"
|
|
|
|
|
|
let is_asyncTask_get =
|
|
is_call_of_class_or_superclass ~actuals_pred:actuals_are_empty_or_timeout
|
|
["android.os.AsyncTask"] "get"
|
|
|
|
|
|
(* at most one function is allowed to be true *)
|
|
let may_block =
|
|
let open StarvationDomain.Event in
|
|
let matchers =
|
|
[ (is_countdownlatch_await, Medium)
|
|
; (is_two_way_binder_transact, High)
|
|
; (is_getWindowVisibleDisplayFrame, Low)
|
|
; (is_future_get, High)
|
|
; (is_accountManager_setUserData, High)
|
|
; (is_asyncTask_get, High) ]
|
|
in
|
|
fun tenv pn actuals ->
|
|
List.find_map matchers ~f:(fun (matcher, sev) -> Option.some_if (matcher tenv pn actuals) sev)
|
|
|
|
|
|
let is_synchronized_library_call =
|
|
let targets = ["java.lang.StringBuffer"; "java.util.Hashtable"; "java.util.Vector"] in
|
|
fun tenv pn ->
|
|
not (Typ.Procname.is_constructor pn)
|
|
&&
|
|
match pn with
|
|
| Typ.Procname.Java java_pname ->
|
|
let classname = Typ.Procname.Java.get_class_type_name java_pname in
|
|
List.exists targets ~f:(PatternMatch.is_subtype_of_str tenv classname)
|
|
| _ ->
|
|
false
|
|
end
|