Reviewed By: mbouaziz Differential Revision: D9631216 fbshipit-source-id: 286366c4amaster
parent
6597d701a1
commit
365ebd8873
@ -0,0 +1,242 @@
|
||||
(*
|
||||
* 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 MF = MarkupFormatter
|
||||
|
||||
type lock = Lock | Unlock | LockedIfTrue | NoEffect
|
||||
|
||||
type thread = BackgroundThread | MainThread | MainThreadIfTrue | UnknownThread
|
||||
|
||||
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_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 Annotations.ia_is_worker_thread
|
||||
|| find_annotated_or_overriden_annotated_method Annotations.ia_is_worker_thread pname tenv
|
||||
|> Option.is_some
|
||||
|| get_current_class_and_annotated_superclasses Annotations.ia_is_worker_thread tenv pname
|
||||
|> Option.value_map ~default:false ~f:(function _, [] -> false | _ -> true)
|
||||
then None
|
||||
else 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 :: _ as super_classes)) ->
|
||||
let middle =
|
||||
if List.exists super_classes ~f:(Typ.Name.equal current_class) then ""
|
||||
else F.asprintf " extends %a, which" (MF.wrap_monospaced Typ.Name.pp) super_class
|
||||
in
|
||||
Some
|
||||
(F.asprintf "class %s%s is annotated %s"
|
||||
(MF.monospaced_to_string (Typ.Name.name current_class))
|
||||
middle
|
||||
(MF.monospaced_to_string Annotations.ui_thread))
|
||||
| _ ->
|
||||
None )
|
||||
|
||||
|
||||
let is_call_of_class ?(search_superclasses = true) ?(method_prefix = false)
|
||||
?(actuals_pred = fun _ -> true) class_names method_name =
|
||||
let is_target_class =
|
||||
let target_set = List.map class_names ~f:Typ.Name.Java.from_string |> Typ.Name.Set.of_list in
|
||||
fun tname -> Typ.Name.Set.mem tname target_set
|
||||
in
|
||||
let is_target_struct tname _ = is_target_class tname in
|
||||
Staged.stage (fun 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
|
||||
( if method_prefix then String.is_prefix mthd ~prefix:method_name
|
||||
else String.equal mthd method_name )
|
||||
&&
|
||||
if search_superclasses then PatternMatch.supertype_exists tenv is_target_struct classname
|
||||
else is_target_class classname
|
||||
| _ ->
|
||||
false )
|
@ -0,0 +1,44 @@
|
||||
(*
|
||||
* 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
|
||||
|
||||
type lock = Lock | Unlock | LockedIfTrue | NoEffect
|
||||
|
||||
type thread = BackgroundThread | MainThread | MainThreadIfTrue | UnknownThread
|
||||
|
||||
val is_thread_utils_method : string -> Typ.Procname.t -> bool
|
||||
(** return true if the given method name is a utility class for checking what thread we're on
|
||||
TODO: clean this up so it takes only a procname *)
|
||||
|
||||
val get_lock : Typ.Procname.t -> HilExp.t list -> lock
|
||||
(** describe how this procedure behaves with respect to locking *)
|
||||
|
||||
val get_thread : Typ.Procname.t -> thread
|
||||
(** describe how this procedure behaves with respect to thread access *)
|
||||
|
||||
val runs_on_ui_thread : Tenv.t -> Procdesc.t -> string option
|
||||
(** We don't want to warn on methods that run on the UI thread because they should always be
|
||||
single-threaded. Assume that methods annotated with @UiThread, @OnEvent, @OnBind, @OnMount,
|
||||
@OnUnbind, @OnUnmount always run on the UI thread. Also assume that any superclass
|
||||
marked @UiThread implies all methods are on UI thread. Return Some string explaining why
|
||||
this method is on the UI thread, else return None. *)
|
||||
|
||||
val get_current_class_and_annotated_superclasses :
|
||||
(Annot.Item.t -> bool) -> Tenv.t -> Typ.Procname.t -> (Typ.name * Typ.name list) option
|
||||
|
||||
val find_annotated_or_overriden_annotated_method :
|
||||
(Annot.Item.t -> bool) -> BuiltinDecl.t -> Tenv.t -> BuiltinDecl.t sexp_option
|
||||
|
||||
val is_call_of_class :
|
||||
?search_superclasses:bool
|
||||
-> ?method_prefix:bool
|
||||
-> ?actuals_pred:('a -> bool)
|
||||
-> string sexp_list
|
||||
-> string
|
||||
-> (Tenv.t -> BuiltinDecl.t -> 'a -> bool) Staged.t
|
@ -1,779 +0,0 @@
|
||||
(*
|
||||
* 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 Annotations.ia_is_worker_thread
|
||||
|| find_annotated_or_overriden_annotated_method Annotations.ia_is_worker_thread pname tenv
|
||||
|> Option.is_some
|
||||
|| get_current_class_and_annotated_superclasses Annotations.ia_is_worker_thread tenv pname
|
||||
|> Option.value_map ~default:false ~f:(function _, [] -> false | _ -> true)
|
||||
then None
|
||||
else 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 :: _ as super_classes)) ->
|
||||
let middle =
|
||||
if List.exists super_classes ~f:(Typ.Name.equal current_class) then ""
|
||||
else F.asprintf " extends %a, which" (MF.wrap_monospaced Typ.Name.pp) super_class
|
||||
in
|
||||
Some
|
||||
(F.asprintf "class %s%s is annotated %s"
|
||||
(MF.monospaced_to_string (Typ.Name.name current_class))
|
||||
middle
|
||||
(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 ?(search_superclasses = true) ?(method_prefix = false)
|
||||
?(actuals_pred = fun _ -> true) class_names method_name =
|
||||
let is_target_class =
|
||||
let target_set = List.map class_names ~f:Typ.Name.Java.from_string |> Typ.Name.Set.of_list in
|
||||
fun tname -> Typ.Name.Set.mem tname target_set
|
||||
in
|
||||
let is_target_struct tname _ = is_target_class tname in
|
||||
Staged.stage (fun 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
|
||||
( if method_prefix then String.is_prefix mthd ~prefix:method_name
|
||||
else String.equal mthd method_name )
|
||||
&&
|
||||
if search_superclasses then
|
||||
PatternMatch.supertype_exists tenv is_target_struct classname
|
||||
else is_target_class classname
|
||||
| _ ->
|
||||
false )
|
||||
|
||||
|
||||
(** magical value from https://developer.android.com/topic/performance/vitals/anr *)
|
||||
let android_anr_time_limit = 5.0
|
||||
|
||||
(* get time unit in seconds *)
|
||||
let timeunit_of_exp =
|
||||
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
|
||||
let str_of_access_path = function
|
||||
| _, [AccessPath.FieldAccess field]
|
||||
when String.equal "java.util.concurrent.TimeUnit" (Typ.Fieldname.Java.get_class field) ->
|
||||
Some (Typ.Fieldname.Java.get_field field)
|
||||
| _ ->
|
||||
None
|
||||
in
|
||||
let str_of_exp = function
|
||||
| HilExp.AccessExpression timeunit_acc_exp ->
|
||||
AccessExpression.to_access_path timeunit_acc_exp |> str_of_access_path
|
||||
| _ ->
|
||||
None
|
||||
in
|
||||
fun timeunit_exp -> str_of_exp timeunit_exp |> Option.bind ~f:(String.Map.find time_units)
|
||||
|
||||
|
||||
(** check whether actuals of a method call either empty, denoting indefinite timeout,
|
||||
or evaluate to a finite timeout greater than the android anr limit *)
|
||||
let empty_or_excessive_timeout actuals =
|
||||
let duration_of_exp = function
|
||||
| HilExp.Constant (Const.Cint duration_lit) ->
|
||||
Some (IntLit.to_float duration_lit)
|
||||
| _ ->
|
||||
None
|
||||
in
|
||||
(* all arguments in seconds *)
|
||||
let is_excessive_secs duration = duration >. android_anr_time_limit in
|
||||
match actuals with
|
||||
| [_] ->
|
||||
(* this is a wait or lock call without timeout, thus it can block indefinitely *)
|
||||
true
|
||||
| [_; snd_arg] ->
|
||||
(* this is an Object.wait(_) call, second argument should be a duration in milliseconds *)
|
||||
duration_of_exp snd_arg
|
||||
|> Option.value_map ~default:false ~f:(fun duration -> is_excessive_secs (0.001 *. duration)
|
||||
)
|
||||
| [_; snd_arg; third_arg] ->
|
||||
(* this is either a call to Object.wait(_, _) or to a java.util.concurent.lock(_, _) method.
|
||||
In the first case the arguments are a duration in milliseconds and an extra duration in
|
||||
nanoseconds; in the second case, the arguments are a duration and a time unit. *)
|
||||
duration_of_exp snd_arg
|
||||
|> Option.value_map ~default:false ~f:(fun duration ->
|
||||
match timeunit_of_exp third_arg with
|
||||
| Some timeunit ->
|
||||
is_excessive_secs (timeunit *. duration)
|
||||
| None ->
|
||||
duration_of_exp third_arg
|
||||
|> Option.value_map ~default:false ~f:(fun extra ->
|
||||
is_excessive_secs (0.001 *. (duration +. (0.000_001 *. extra))) ) )
|
||||
| _ ->
|
||||
false
|
||||
|
||||
|
||||
(** is the method called Object.wait or on subclass, without timeout or with excessive timeout ? *)
|
||||
let is_object_wait =
|
||||
is_call_of_class ~actuals_pred:empty_or_excessive_timeout ["java.lang.Object"] "wait"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
(** is the method called CountDownLath.await or on subclass? *)
|
||||
let is_countdownlatch_await =
|
||||
is_call_of_class ~actuals_pred:empty_or_excessive_timeout
|
||||
["java.util.concurrent.CountDownLatch"]
|
||||
"await"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
(** 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 ~actuals_pred ["android.os.IBinder"] "transact" |> Staged.unstage
|
||||
|
||||
|
||||
(** is it a call to Future.get()? *)
|
||||
let is_future_get =
|
||||
is_call_of_class ~search_superclasses:false ~actuals_pred:empty_or_excessive_timeout
|
||||
["java.util.concurrent.Future"] "get"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
let is_accountManager_setUserData =
|
||||
is_call_of_class ~search_superclasses:false ["android.accounts.AccountManager"] "setUserData"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
let is_asyncTask_get =
|
||||
is_call_of_class ~actuals_pred:empty_or_excessive_timeout ["android.os.AsyncTask"] "get"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
(* consider any call to sleep as bad, even with timeouts lower than the anr limit *)
|
||||
let is_thread_sleep = is_call_of_class ["java.lang.Thread"] "sleep" |> Staged.unstage
|
||||
|
||||
(* at most one function is allowed to be true, sort from High to Low *)
|
||||
let may_block =
|
||||
let open StarvationDomain.Event in
|
||||
let matchers =
|
||||
[ (is_accountManager_setUserData, High)
|
||||
; (is_two_way_binder_transact, High)
|
||||
; (is_countdownlatch_await, High)
|
||||
; (is_thread_sleep, High)
|
||||
; (is_object_wait, High)
|
||||
; (is_asyncTask_get, Low)
|
||||
; (is_future_get, Low) ]
|
||||
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
|
||||
|
||||
|
||||
let is_futures_getdone =
|
||||
is_call_of_class ["com.google.common.util.concurrent.Futures"] "getDone" |> Staged.unstage
|
||||
|
||||
|
||||
let should_skip_analysis =
|
||||
let matchers = [is_futures_getdone] in
|
||||
fun tenv pn actuals -> List.exists matchers ~f:(fun matcher -> matcher tenv pn actuals)
|
||||
end
|
@ -1,91 +0,0 @@
|
||||
(*
|
||||
* 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 Models : sig
|
||||
type lock = Lock | Unlock | LockedIfTrue | NoEffect
|
||||
|
||||
type thread = BackgroundThread | MainThread | MainThreadIfTrue | UnknownThread
|
||||
|
||||
type container_access = ContainerRead | ContainerWrite
|
||||
|
||||
val is_thread_utils_method : string -> Typ.Procname.t -> bool
|
||||
(** return true if the given method name is a utility class for checking what thread we're on
|
||||
TODO: clean this up so it takes only a procname *)
|
||||
|
||||
val get_lock : Typ.Procname.t -> HilExp.t list -> lock
|
||||
(** describe how this procedure behaves with respect to locking *)
|
||||
|
||||
val get_thread : Typ.Procname.t -> thread
|
||||
(** describe how this procedure behaves with respect to thread access *)
|
||||
|
||||
val get_container_access : Typ.Procname.t -> Tenv.t -> container_access option
|
||||
(** return Some (access) if this procedure accesses the contents of a container (e.g., Map.get) *)
|
||||
|
||||
val is_java_library : Typ.Procname.t -> bool
|
||||
(** return true if this function is library code from the JDK core libraries or Android *)
|
||||
|
||||
val is_builder_function : Typ.Procname.t -> bool
|
||||
|
||||
val has_return_annot : (Annot.Item.t -> bool) -> Typ.Procname.t -> bool
|
||||
|
||||
val is_functional : Typ.Procname.t -> bool
|
||||
|
||||
val acquires_ownership : Typ.Procname.t -> Tenv.t -> bool
|
||||
|
||||
val is_threadsafe_collection : Typ.Procname.t -> Tenv.t -> bool
|
||||
|
||||
val is_box : Typ.Procname.t -> bool
|
||||
(** return true if the given procname boxes a primitive type into a reference type *)
|
||||
|
||||
val is_thread_confined_method : Tenv.t -> Procdesc.t -> bool
|
||||
(** 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. *)
|
||||
|
||||
val runs_on_ui_thread : Tenv.t -> Procdesc.t -> string option
|
||||
(** We don't want to warn on methods that run on the UI thread because they should always be
|
||||
single-threaded. Assume that methods annotated with @UiThread, @OnEvent, @OnBind, @OnMount,
|
||||
@OnUnbind, @OnUnmount always run on the UI thread. Also assume that any superclass
|
||||
marked @UiThread implies all methods are on UI thread. Return Some string explaining why
|
||||
this method is on the UI thread, else return None. *)
|
||||
|
||||
val should_analyze_proc : Procdesc.t -> Tenv.t -> bool
|
||||
(** 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 *)
|
||||
|
||||
val get_current_class_and_threadsafe_superclasses :
|
||||
Tenv.t -> Typ.Procname.t -> (Typ.name * Typ.name list) option
|
||||
|
||||
val is_thread_safe_method : Typ.Procname.t -> Tenv.t -> bool
|
||||
(** returns true if method or overriden method in superclass
|
||||
is @ThreadSafe, @ThreadSafe(enableChecks = true), or is defined
|
||||
as an alias of @ThreadSafe in a .inferconfig file. *)
|
||||
|
||||
val is_marked_thread_safe : Procdesc.t -> Tenv.t -> bool
|
||||
|
||||
val should_report_on_proc : Procdesc.t -> Tenv.t -> bool
|
||||
(** return true if procedure is at an abstraction boundary or reporting has been explicitly
|
||||
requested via @ThreadSafe *)
|
||||
|
||||
val may_block :
|
||||
Tenv.t -> Typ.Procname.t -> HilExp.t list -> StarvationDomain.Event.severity_t option
|
||||
(** is the method call potentially blocking, given the actuals passed? *)
|
||||
|
||||
val is_synchronized_library_call : Tenv.t -> Typ.Procname.t -> bool
|
||||
(** does the method call lock-then-unlock the underlying object?
|
||||
legacy Java containers like Vector do this, and can interact with explicit locking *)
|
||||
|
||||
val should_skip_analysis : Tenv.t -> Typ.Procname.t -> HilExp.t list -> bool
|
||||
(** should we go avoid analyzing a library method (eg in guava) to avoid FPs? *)
|
||||
end
|
@ -0,0 +1,385 @@
|
||||
(*
|
||||
* 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 L = Logging
|
||||
open ConcurrencyModels
|
||||
|
||||
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
|
||||
|
||||
type container_access = ContainerRead | ContainerWrite
|
||||
|
||||
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_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)
|
@ -0,0 +1,56 @@
|
||||
(*
|
||||
* 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
|
||||
|
||||
type container_access = ContainerRead | ContainerWrite
|
||||
|
||||
val get_container_access : Typ.Procname.t -> Tenv.t -> container_access option
|
||||
(** return Some (access) if this procedure accesses the contents of a container (e.g., Map.get) *)
|
||||
|
||||
val is_java_library : Typ.Procname.t -> bool
|
||||
(** return true if this function is library code from the JDK core libraries or Android *)
|
||||
|
||||
val is_builder_function : Typ.Procname.t -> bool
|
||||
|
||||
val has_return_annot : (Annot.Item.t -> bool) -> Typ.Procname.t -> bool
|
||||
|
||||
val is_functional : Typ.Procname.t -> bool
|
||||
|
||||
val acquires_ownership : Typ.Procname.t -> Tenv.t -> bool
|
||||
|
||||
val is_threadsafe_collection : Typ.Procname.t -> Tenv.t -> bool
|
||||
|
||||
val is_box : Typ.Procname.t -> bool
|
||||
(** return true if the given procname boxes a primitive type into a reference type *)
|
||||
|
||||
val is_thread_confined_method : Tenv.t -> Procdesc.t -> bool
|
||||
(** 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. *)
|
||||
|
||||
val should_analyze_proc : Procdesc.t -> Tenv.t -> bool
|
||||
(** 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 *)
|
||||
|
||||
val get_current_class_and_threadsafe_superclasses :
|
||||
Tenv.t -> Typ.Procname.t -> (Typ.name * Typ.name list) option
|
||||
|
||||
val is_thread_safe_method : Typ.Procname.t -> Tenv.t -> bool
|
||||
(** returns true if method or overriden method in superclass
|
||||
is @ThreadSafe, @ThreadSafe(enableChecks = true), or is defined
|
||||
as an alias of @ThreadSafe in a .inferconfig file. *)
|
||||
|
||||
val is_marked_thread_safe : Procdesc.t -> Tenv.t -> bool
|
||||
|
||||
val should_report_on_proc : Procdesc.t -> Tenv.t -> bool
|
||||
(** return true if procedure is at an abstraction boundary or reporting has been explicitly
|
||||
requested via @ThreadSafe *)
|
@ -0,0 +1,158 @@
|
||||
(*
|
||||
* Copyright (c) 2018-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
|
||||
open ConcurrencyModels
|
||||
|
||||
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
|
||||
|
||||
|
||||
let is_futures_getdone =
|
||||
is_call_of_class ["com.google.common.util.concurrent.Futures"] "getDone" |> Staged.unstage
|
||||
|
||||
|
||||
let should_skip_analysis =
|
||||
let matchers = [is_futures_getdone] in
|
||||
fun tenv pn actuals -> List.exists matchers ~f:(fun matcher -> matcher tenv pn actuals)
|
||||
|
||||
|
||||
(** magical value from https://developer.android.com/topic/performance/vitals/anr *)
|
||||
let android_anr_time_limit = 5.0
|
||||
|
||||
(* get time unit in seconds *)
|
||||
let timeunit_of_exp =
|
||||
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
|
||||
let str_of_access_path = function
|
||||
| _, [AccessPath.FieldAccess field]
|
||||
when String.equal "java.util.concurrent.TimeUnit" (Typ.Fieldname.Java.get_class field) ->
|
||||
Some (Typ.Fieldname.Java.get_field field)
|
||||
| _ ->
|
||||
None
|
||||
in
|
||||
let str_of_exp = function
|
||||
| HilExp.AccessExpression timeunit_acc_exp ->
|
||||
AccessExpression.to_access_path timeunit_acc_exp |> str_of_access_path
|
||||
| _ ->
|
||||
None
|
||||
in
|
||||
fun timeunit_exp -> str_of_exp timeunit_exp |> Option.bind ~f:(String.Map.find time_units)
|
||||
|
||||
|
||||
(** check whether actuals of a method call either empty, denoting indefinite timeout,
|
||||
or evaluate to a finite timeout greater than the android anr limit *)
|
||||
let empty_or_excessive_timeout actuals =
|
||||
let duration_of_exp = function
|
||||
| HilExp.Constant (Const.Cint duration_lit) ->
|
||||
Some (IntLit.to_float duration_lit)
|
||||
| _ ->
|
||||
None
|
||||
in
|
||||
(* all arguments in seconds *)
|
||||
let is_excessive_secs duration = duration >. android_anr_time_limit in
|
||||
match actuals with
|
||||
| [_] ->
|
||||
(* this is a wait or lock call without timeout, thus it can block indefinitely *)
|
||||
true
|
||||
| [_; snd_arg] ->
|
||||
(* this is an Object.wait(_) call, second argument should be a duration in milliseconds *)
|
||||
duration_of_exp snd_arg
|
||||
|> Option.value_map ~default:false ~f:(fun duration -> is_excessive_secs (0.001 *. duration))
|
||||
| [_; snd_arg; third_arg] ->
|
||||
(* this is either a call to Object.wait(_, _) or to a java.util.concurent.lock(_, _) method.
|
||||
In the first case the arguments are a duration in milliseconds and an extra duration in
|
||||
nanoseconds; in the second case, the arguments are a duration and a time unit. *)
|
||||
duration_of_exp snd_arg
|
||||
|> Option.value_map ~default:false ~f:(fun duration ->
|
||||
match timeunit_of_exp third_arg with
|
||||
| Some timeunit ->
|
||||
is_excessive_secs (timeunit *. duration)
|
||||
| None ->
|
||||
duration_of_exp third_arg
|
||||
|> Option.value_map ~default:false ~f:(fun extra ->
|
||||
is_excessive_secs (0.001 *. (duration +. (0.000_001 *. extra))) ) )
|
||||
| _ ->
|
||||
false
|
||||
|
||||
|
||||
(** is the method called Object.wait or on subclass, without timeout or with excessive timeout ? *)
|
||||
let is_object_wait =
|
||||
is_call_of_class ~actuals_pred:empty_or_excessive_timeout ["java.lang.Object"] "wait"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
(** is the method called CountDownLath.await or on subclass? *)
|
||||
let is_countdownlatch_await =
|
||||
is_call_of_class ~actuals_pred:empty_or_excessive_timeout
|
||||
["java.util.concurrent.CountDownLatch"]
|
||||
"await"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
(** 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 ~actuals_pred ["android.os.IBinder"] "transact" |> Staged.unstage
|
||||
|
||||
|
||||
(** is it a call to Future.get()? *)
|
||||
let is_future_get =
|
||||
is_call_of_class ~search_superclasses:false ~actuals_pred:empty_or_excessive_timeout
|
||||
["java.util.concurrent.Future"] "get"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
let is_accountManager_setUserData =
|
||||
is_call_of_class ~search_superclasses:false ["android.accounts.AccountManager"] "setUserData"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
let is_asyncTask_get =
|
||||
is_call_of_class ~actuals_pred:empty_or_excessive_timeout ["android.os.AsyncTask"] "get"
|
||||
|> Staged.unstage
|
||||
|
||||
|
||||
(* consider any call to sleep as bad, even with timeouts lower than the anr limit *)
|
||||
let is_thread_sleep = is_call_of_class ["java.lang.Thread"] "sleep" |> Staged.unstage
|
||||
|
||||
(* at most one function is allowed to be true, sort from High to Low *)
|
||||
let may_block =
|
||||
let open StarvationDomain.Event in
|
||||
let matchers =
|
||||
[ (is_accountManager_setUserData, High)
|
||||
; (is_two_way_binder_transact, High)
|
||||
; (is_countdownlatch_await, High)
|
||||
; (is_thread_sleep, High)
|
||||
; (is_object_wait, High)
|
||||
; (is_asyncTask_get, Low)
|
||||
; (is_future_get, Low) ]
|
||||
in
|
||||
fun tenv pn actuals ->
|
||||
List.find_map matchers ~f:(fun (matcher, sev) -> Option.some_if (matcher tenv pn actuals) sev)
|
@ -0,0 +1,19 @@
|
||||
(*
|
||||
* Copyright (c) 2018-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
|
||||
|
||||
val may_block :
|
||||
Tenv.t -> Typ.Procname.t -> HilExp.t list -> StarvationDomain.Event.severity_t option
|
||||
(** is the method call potentially blocking, given the actuals passed? *)
|
||||
|
||||
val is_synchronized_library_call : Tenv.t -> Typ.Procname.t -> bool
|
||||
(** does the method call lock-then-unlock the underlying object?
|
||||
legacy Java containers like Vector do this, and can interact with explicit locking *)
|
||||
|
||||
val should_skip_analysis : Tenv.t -> Typ.Procname.t -> HilExp.t list -> bool
|
||||
(** should we go avoid analyzing a library method (eg in guava) to avoid FPs? *)
|
Loading…
Reference in new issue