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.
454 lines
16 KiB
454 lines
16 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 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 make_android_support_template suffix methods =
|
|
let open MethodMatcher in
|
|
[ {default with classname= "android.support.v4.util." ^ suffix; methods}
|
|
; {default with classname= "androidx.core.util." ^ suffix; methods} ]
|
|
|
|
|
|
let is_java_container_write =
|
|
let open MethodMatcher in
|
|
let array_methods =
|
|
["append"; "clear"; "delete"; "put"; "remove"; "removeAt"; "removeAtRange"; "setValueAt"]
|
|
in
|
|
make_android_support_template "Pools$SimplePool" ["acquire"; "release"]
|
|
@ make_android_support_template "SimpleArrayMap"
|
|
["clear"; "ensureCapacity"; "put"; "putAll"; "remove"; "removeAt"; "setValueAt"]
|
|
@ make_android_support_template "SparseArrayCompat" array_methods
|
|
@ [ {default with classname= "android.util.SparseArray"; methods= array_methods}
|
|
; { default with
|
|
classname= "java.util.List"; methods= ["add"; "addAll"; "clear"; "remove"; "set"] }
|
|
; {default with classname= "java.util.Map"; methods= ["clear"; "put"; "putAll"; "remove"]} ]
|
|
|> of_records
|
|
|
|
|
|
let is_java_container_read =
|
|
let open MethodMatcher in
|
|
let array_methods = ["clone"; "get"; "indexOfKey"; "indexOfValue"; "keyAt"; "size"; "valueAt"] in
|
|
make_android_support_template "SimpleArrayMap"
|
|
[ "containsKey"
|
|
; "containsValue"
|
|
; "get"
|
|
; "hashCode"
|
|
; "indexOfKey"
|
|
; "isEmpty"
|
|
; "keyAt"
|
|
; "size"
|
|
; "valueAt" ]
|
|
@ make_android_support_template "SparseArrayCompat" array_methods
|
|
@ [ {default with classname= "android.util.SparseArray"; methods= array_methods}
|
|
; { default with
|
|
classname= "java.util.List"
|
|
; methods=
|
|
[ "contains"
|
|
; "containsAll"
|
|
; "equals"
|
|
; "get"
|
|
; "hashCode"
|
|
; "indexOf"
|
|
; "isEmpty"
|
|
; "iterator"
|
|
; "lastIndexOf"
|
|
; "listIterator"
|
|
; "size"
|
|
; "toArray" ] }
|
|
; { default with
|
|
classname= "java.util.Map"
|
|
; methods=
|
|
[ "containsKey"
|
|
; "containsValue"
|
|
; "entrySet"
|
|
; "equals"
|
|
; "get"
|
|
; "hashCode"
|
|
; "isEmpty"
|
|
; "keySet"
|
|
; "size"
|
|
; "values" ] } ]
|
|
|> of_records
|
|
|
|
|
|
let is_cpp_container_read =
|
|
let is_container_operator pname_qualifiers =
|
|
QualifiedCppName.extract_last pname_qualifiers
|
|
|> Option.exists ~f:(fun (last, _) -> String.equal last "operator[]")
|
|
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
|
|
|
|
|
|
let 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)
|
|
|
|
|
|
let get_container_access pn tenv =
|
|
match pn with
|
|
| Typ.Procname.Java _ when is_java_container_write tenv pn [] ->
|
|
Some ContainerWrite
|
|
| Typ.Procname.Java _ when is_java_container_read tenv pn [] ->
|
|
Some ContainerRead
|
|
| Typ.Procname.Java _ ->
|
|
None
|
|
(* 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 _) when is_cpp_container_write pn ->
|
|
Some ContainerWrite
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) when is_cpp_container_read pn ->
|
|
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
|
|
|
|
|
|
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 nsobject = Typ.Name.Objc.from_qual_name (QualifiedCppName.of_qual_string "NSObject")
|
|
|
|
let acquires_ownership pname tenv =
|
|
let is_nsobject_init = function
|
|
| Typ.Procname.ObjC_Cpp
|
|
{kind= Typ.Procname.ObjC_Cpp.ObjCInstanceMethod; method_name= "init"; class_name} ->
|
|
Typ.Name.equal class_name nsobject
|
|
| _ ->
|
|
false
|
|
in
|
|
let is_allocation pn =
|
|
Typ.Procname.equal pn BuiltinDecl.__new
|
|
|| Typ.Procname.equal pn BuiltinDecl.__new_array
|
|
|| is_nsobject_init pn
|
|
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"
|
|
| "androidx.core.util.Pools$Pool"
|
|
| "androidx.core.util.Pools$SimplePool"
|
|
| "androidx.core.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
|
|
|
|
|
|
(* 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 ~attrs_of_pname:Summary.proc_resolve_attributes
|
|
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
|
|
|
|
|
|
let is_safe_access access prefix_path tenv =
|
|
match (access, AccessPath.get_typ prefix_path tenv) with
|
|
| ( AccessPath.FieldAccess fieldname
|
|
, Some ({Typ.desc= Tstruct typename} | {desc= Tptr ({desc= Tstruct typename}, _)}) ) -> (
|
|
match Tenv.lookup tenv typename with
|
|
| Some struct_typ ->
|
|
Annotations.struct_typ_has_annot struct_typ Annotations.ia_is_thread_confined
|
|
|| Annotations.field_has_annot fieldname struct_typ Annotations.ia_is_thread_confined
|
|
|| Annotations.field_has_annot fieldname struct_typ Annotations.ia_is_volatile
|
|
| None ->
|
|
false )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let should_flag_interface_call tenv exp call_flags pname =
|
|
(* 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
|
|
in
|
|
let is_builder_function = function
|
|
| Typ.Procname.Java java_pname ->
|
|
String.is_suffix ~suffix:"$Builder" (Typ.Procname.Java.get_class_name java_pname)
|
|
| _ ->
|
|
false
|
|
in
|
|
let thread_safe_or_thread_confined annot =
|
|
Annotations.ia_is_thread_safe annot || Annotations.ia_is_thread_confined annot
|
|
in
|
|
call_flags.CallFlags.cf_interface && Typ.Procname.is_java pname
|
|
&& (not (is_java_library pname || is_builder_function pname))
|
|
(* can't ask anyone to annotate interfaces in library code, and Builder's should always be
|
|
thread-safe (would be unreasonable to ask everyone to annotate them) *)
|
|
&& (not (PatternMatch.check_class_attributes thread_safe_or_thread_confined tenv pname))
|
|
&& (not (has_return_annot thread_safe_or_thread_confined pname))
|
|
&&
|
|
match exp with
|
|
| HilExp.AccessExpression receiver_access_exp :: _ -> (
|
|
HilExp.AccessExpression.to_access_path receiver_access_exp
|
|
|> AccessPath.truncate
|
|
|> function
|
|
| receiver_prefix, Some receiver_field ->
|
|
is_safe_access receiver_field receiver_prefix tenv |> not
|
|
| _ ->
|
|
true )
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let is_synchronized_container callee_pname ((_, (base_typ : Typ.t)), accesses) tenv =
|
|
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"
|
|
| "androidx.core.util.Pools$SynchronizedPool" ->
|
|
true
|
|
| _ ->
|
|
false
|
|
in
|
|
PatternMatch.supertype_exists tenv aux typename
|
|
| _ ->
|
|
false
|
|
in
|
|
if is_threadsafe_collection callee_pname tenv then true
|
|
else
|
|
let is_annotated_synchronized base_typename container_field tenv =
|
|
match Tenv.lookup tenv base_typename with
|
|
| Some base_typ ->
|
|
Annotations.field_has_annot container_field base_typ
|
|
Annotations.ia_is_synchronized_collection
|
|
| None ->
|
|
false
|
|
in
|
|
match List.rev accesses with
|
|
| AccessPath.FieldAccess base_field :: AccessPath.FieldAccess container_field :: _
|
|
when Typ.Procname.is_java callee_pname ->
|
|
let base_typename = Typ.Name.Java.from_string (Typ.Fieldname.Java.get_class base_field) in
|
|
is_annotated_synchronized base_typename container_field tenv
|
|
| [AccessPath.FieldAccess container_field] -> (
|
|
match base_typ.desc with
|
|
| Typ.Tstruct base_typename | Tptr ({Typ.desc= Tstruct base_typename}, _) ->
|
|
is_annotated_synchronized base_typename container_field tenv
|
|
| _ ->
|
|
false )
|
|
| _ ->
|
|
false
|