(* * Copyright (c) 2016 - present Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. *) open! IStd module F = Format module L = Logging module Summary = Summary.Make (struct type summary = ThreadSafetyDomain.summary let update_payload summary payload = { payload with Specs.threadsafety = Some summary } let read_from_payload payload = payload.Specs.threadsafety end) module TransferFunctions (CFG : ProcCfg.S) = struct module CFG = CFG module Domain = ThreadSafetyDomain type extras = FormalMap.t type lock_model = | Lock | Unlock | NoEffect let get_lock_model = function | Procname.Java java_pname -> begin match Procname.java_get_class_name java_pname, Procname.java_get_method java_pname with | "java.util.concurrent.locks.Lock", "lock" -> Lock | ("java.util.concurrent.locks.ReentrantLock" | "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock" | "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock"), ("lock" | "tryLock" | "lockInterruptibly") -> Lock | ("java.util.concurrent.locks.Lock" |"java.util.concurrent.locks.ReentrantLock" | "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock" | "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock"), "unlock" -> Unlock | _ -> NoEffect end | pname when Procname.equal pname BuiltinDecl.__set_locked_attribute -> Lock | pname when Procname.equal pname BuiltinDecl.__delete_locked_attribute -> Unlock | _ -> NoEffect let resolve_id (id_map : IdAccessPathMapDomain.astate) id = try Some (IdAccessPathMapDomain.find id id_map) with Not_found -> None let is_constant = function | Exp.Const _ -> true | _ -> false let propagate_attributes lhs_access_path_opt rhs_access_path_opt rhs_exp attribute_map = match lhs_access_path_opt, rhs_access_path_opt with | Some lhs_access_path, Some rhs_access_path -> let rhs_attributes = try Domain.AttributeMapDomain.find rhs_access_path attribute_map with Not_found -> Domain.AttributeSetDomain.empty in Domain.AttributeMapDomain.add lhs_access_path rhs_attributes attribute_map | Some lhs_access_path, None -> let rhs_attributes = if is_constant rhs_exp then (* constants are both owned and functional *) Domain.AttributeSetDomain.of_list [Domain.Attribute.Owned; Domain.Attribute.Functional] else Domain.AttributeSetDomain.empty in Domain.AttributeMapDomain.add lhs_access_path rhs_attributes attribute_map | _ -> attribute_map let add_path_to_state exp typ loc path_state id_map attribute_map tenv = (* remove the last field of the access path, if it has any *) let truncate = function | base, [] | base, _ :: [] -> base, [] | base, accesses -> base, IList.rev (IList.tl (IList.rev accesses)) in (* we don't want to warn on writes to the field if it is (a) thread-confined, or (b) volatile *) let is_safe_write access_path tenv = let is_thread_safe_write accesses tenv = match IList.rev accesses with | AccessPath.FieldAccess (fieldname, Typ.Tstruct typename) :: _ -> begin match Tenv.lookup tenv typename with | Some struct_typ -> Annotations.struct_typ_has_annot struct_typ Annotations.ia_is_thread_confined || Annotations.field_has_annot fieldname struct_typ Annotations.ia_is_thread_confined || Annotations.field_has_annot fieldname struct_typ Annotations.ia_is_volatile | None -> false end | _ -> false in is_thread_safe_write (snd access_path) tenv in let f_resolve_id = resolve_id id_map in if is_constant exp then path_state else IList.fold_left (fun acc rawpath -> if not (Domain.AttributeMapDomain.has_attribute (truncate rawpath) Domain.Attribute.Owned attribute_map) && not (is_safe_write rawpath tenv) then Domain.PathDomain.add_sink (Domain.make_access rawpath loc) acc else acc) path_state (AccessPath.of_exp exp typ ~f_resolve_id) let analyze_id_assignment lhs_id rhs_exp rhs_typ { Domain.id_map; } = let f_resolve_id = resolve_id id_map in match AccessPath.of_lhs_exp rhs_exp rhs_typ ~f_resolve_id with | Some rhs_access_path -> IdAccessPathMapDomain.add lhs_id rhs_access_path id_map | None -> id_map (* like PatternMatch.override_exists, but also applies [predicate] to [pname] *) let proc_or_override_is_annotated pname tenv predicate = let has_return_annot pn = Annotations.pname_has_return_annot pn ~attrs_of_pname:Specs.proc_resolve_attributes predicate in has_return_annot pname || PatternMatch.override_exists has_return_annot tenv pname let exec_instr (astate : Domain.astate) { ProcData.pdesc; tenv; extras; } _ = let is_allocation pn = Procname.equal pn BuiltinDecl.__new || Procname.equal pn BuiltinDecl.__new_array in let is_container_write pn tenv = match pn with | Procname.Java java_pname -> let typename = Typename.Java.from_string (Procname.java_get_class_name java_pname) in let is_container_write_ typename _ = match Typename.name typename, Procname.java_get_method java_pname with | "java.util.List", ("add" | "addAll" | "clear" | "remove" | "set") -> true | "java.util.Map", ("clear" | "put" | "putAll" | "remove") -> true | _ -> false in let is_threadsafe_collection typename _ = match Typename.name typename with | "java.util.concurrent.ConcurrentMap" | "java.util.concurrent.CopyOnWriteArrayList" -> true | _ -> false in PatternMatch.supertype_exists tenv is_container_write_ typename && not (PatternMatch.supertype_exists tenv is_threadsafe_collection typename) | _ -> false in let add_container_write pn loc exp typ (astate : Domain.astate) = let dummy_fieldname = Ident.create_fieldname (Mangled.from_string (Procname.get_method pn)) 0 in let dummy_access_exp = Exp.Lfield (exp, dummy_fieldname, typ) in let unconditional_writes = add_path_to_state dummy_access_exp typ loc astate.unconditional_writes astate.id_map astate.attribute_map tenv in { astate with unconditional_writes; } in let is_unprotected is_locked = not is_locked && not (Procdesc.is_java_synchronized pdesc) in (* return true if the given procname boxes a primitive type into a reference type *) let is_box = function | Procname.Java java_pname -> begin match Procname.java_get_class_name java_pname, Procname.java_get_method java_pname with | ("java.lang.Boolean" | "java.lang.Byte" | "java.lang.Char" | "java.lang.Double" | "java.lang.Float" | "java.lang.Integer" | "java.lang.Long" | "java.lang.Short"), "valueOf" -> true | _ -> false end | _ -> false in let f_resolve_id = resolve_id astate.id_map in let open Domain in function | Sil.Call (Some (lhs_id, lhs_typ), Const (Cfun pn), _, _, _) when is_allocation pn -> begin match AccessPath.of_lhs_exp (Exp.Var lhs_id) lhs_typ ~f_resolve_id with | Some lhs_access_path -> let attribute_map = AttributeMapDomain.add_attribute lhs_access_path Owned astate.attribute_map in { astate with attribute_map; } | None -> astate end | Sil.Call (ret_opt, Const (Cfun callee_pname), actuals, loc, _) -> let astate_callee = (* assuming that modeled procedures do not have useful summaries *) match get_lock_model callee_pname with | Lock -> { astate with locks = true; } | Unlock -> { astate with locks = false; } | NoEffect -> if is_unprotected astate.locks && is_container_write callee_pname tenv then match actuals with | (receiver_exp, receiver_typ) :: _ -> add_container_write callee_pname loc receiver_exp receiver_typ astate | [] -> failwithf "Call to %a is marked as a container write, but has no receiver" Procname.pp callee_pname else match Summary.read_summary pdesc callee_pname with | Some (callee_locks, callee_reads, callee_conditional_writes, callee_unconditional_writes, is_retval_owned) -> let locks' = callee_locks || astate.locks in let astate' = (* TODO (14842325): report on constructors that aren't threadsafe (e.g., constructors that access static fields) *) if is_unprotected locks' then let call_site = CallSite.make callee_pname loc in (* add the conditional writes rooted in the callee formal at [index] to the current state *) let add_conditional_writes ((cond_writes, uncond_writes) as acc) index (actual_exp, actual_typ) = if is_constant actual_exp then acc else try let callee_cond_writes_for_index' = let callee_cond_writes_for_index = ConditionalWritesDomain.find index callee_conditional_writes in PathDomain.with_callsite callee_cond_writes_for_index call_site in begin match AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id with | Some actual_access_path -> if AttributeMapDomain.has_attribute actual_access_path Owned astate.attribute_map then (* the actual passed to the current callee is owned. drop all the conditional writes for that actual, since they're all safe *) acc else let base = fst actual_access_path in begin match FormalMap.get_formal_index base extras with | Some formal_index -> (* the actual passed to the current callee is rooted in a formal. add to conditional writes *) let conditional_writes' = try ConditionalWritesDomain.find formal_index cond_writes |> PathDomain.join callee_cond_writes_for_index' with Not_found -> callee_cond_writes_for_index' in let cond_writes' = ConditionalWritesDomain.add formal_index conditional_writes' cond_writes in cond_writes', uncond_writes | None -> (* access path not owned and not rooted in a formal. add to unconditional writes *) cond_writes, PathDomain.join uncond_writes callee_cond_writes_for_index' end | _ -> cond_writes, PathDomain.join uncond_writes callee_cond_writes_for_index' end with Not_found -> acc in let conditional_writes, unconditional_writes = let combined_unconditional_writes = PathDomain.with_callsite callee_unconditional_writes call_site |> PathDomain.join astate.unconditional_writes in IList.fold_lefti add_conditional_writes (astate.conditional_writes, combined_unconditional_writes) actuals in let reads = PathDomain.with_callsite callee_reads call_site |> PathDomain.join astate.reads in { astate with reads; conditional_writes; unconditional_writes; } else astate in let attribute_map = match ret_opt with | Some (ret_id, ret_typ) when is_retval_owned -> AttributeMapDomain.add_attribute (AccessPath.of_id ret_id ret_typ) Owned astate'.attribute_map | _ -> astate'.attribute_map in { astate' with locks = locks'; attribute_map; } | None -> if is_box callee_pname then match ret_opt, actuals with | Some (ret_id, ret_typ), (actual_exp, actual_typ) :: _ -> begin match AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id with | Some ap when AttributeMapDomain.has_attribute ap Functional astate.attribute_map -> let attribute_map = AttributeMapDomain.add_attribute (AccessPath.of_id ret_id ret_typ) Functional astate.attribute_map in { astate with attribute_map; } | _ -> astate end | _ -> astate else astate in begin match ret_opt with | Some (_, (Typ.Tint ILong | Tfloat FDouble)) -> (* writes to longs and doubles are not guaranteed to be atomic in Java, so don't bother tracking whether a returned long or float value is functional *) astate_callee | Some (ret_id, ret_typ) when proc_or_override_is_annotated callee_pname tenv Annotations.ia_is_functional -> let attribute_map = AttributeMapDomain.add_attribute (AccessPath.of_id ret_id ret_typ) Functional astate.attribute_map in { astate_callee with attribute_map; } | _ -> astate_callee end | Sil.Store (Exp.Lvar lhs_pvar, lhs_typ, rhs_exp, _) when Pvar.is_frontend_tmp lhs_pvar -> let id_map' = analyze_id_assignment (Var.of_pvar lhs_pvar) rhs_exp lhs_typ astate in { astate with id_map = id_map'; } | Sil.Store (lhs_exp, lhs_typ, rhs_exp, loc) -> let get_formal_index exp typ = match AccessPath.of_lhs_exp exp typ ~f_resolve_id with | Some (base, _) -> FormalMap.get_formal_index base extras | None -> None in let is_marked_functional exp typ attribute_map = match AccessPath.of_lhs_exp exp typ ~f_resolve_id with | Some access_path -> AttributeMapDomain.has_attribute access_path Functional attribute_map | None -> false in let conditional_writes, unconditional_writes = match lhs_exp with | Lfield (base_exp, _, typ) when is_unprotected astate.locks (* abstracts no lock being held *) && not (is_marked_functional rhs_exp lhs_typ astate.attribute_map) -> begin match get_formal_index base_exp typ with | Some formal_index -> let conditional_writes_for_index = try ConditionalWritesDomain.find formal_index astate.conditional_writes with Not_found -> PathDomain.empty in let conditional_writes_for_index' = add_path_to_state lhs_exp typ loc conditional_writes_for_index astate.id_map astate.attribute_map tenv in ConditionalWritesDomain.add formal_index conditional_writes_for_index' astate.conditional_writes, astate.unconditional_writes | None -> astate.conditional_writes, add_path_to_state lhs_exp typ loc astate.unconditional_writes astate.id_map astate.attribute_map tenv end | _ -> astate.conditional_writes, astate.unconditional_writes in (* if rhs is owned/functional, propagate to lhs. otherwise, remove lhs from ownership/functional set (since it may have previously held an owned/functional memory loc and is now being reassigned *) let lhs_access_path_opt = AccessPath.of_lhs_exp lhs_exp lhs_typ ~f_resolve_id in let rhs_access_path_opt = AccessPath.of_lhs_exp rhs_exp lhs_typ ~f_resolve_id in let attribute_map = propagate_attributes lhs_access_path_opt rhs_access_path_opt rhs_exp astate.attribute_map in { astate with conditional_writes; unconditional_writes; attribute_map; } | Sil.Load (lhs_id, rhs_exp, rhs_typ, loc) -> let id_map = analyze_id_assignment (Var.of_id lhs_id) rhs_exp rhs_typ astate in let reads = match rhs_exp with | Lfield ( _, _, typ) when is_unprotected astate.locks -> add_path_to_state rhs_exp typ loc astate.reads astate.id_map astate.attribute_map tenv | _ -> astate.reads in (* if rhs is owned/functional, propagate to lhs *) let lhs_access_path_opt = Some (AccessPath.of_id lhs_id rhs_typ) in let rhs_access_path_opt = AccessPath.of_lhs_exp rhs_exp rhs_typ ~f_resolve_id in let attribute_map = propagate_attributes lhs_access_path_opt rhs_access_path_opt rhs_exp astate.attribute_map in { astate with Domain.reads; id_map; attribute_map; } | Sil.Remove_temps (ids, _) -> let id_map = IList.fold_left (fun acc id -> IdAccessPathMapDomain.remove (Var.of_id id) acc) astate.id_map ids in { astate with id_map; } | _ -> astate end module Analyzer = AbstractInterpreter.Make (ProcCfg.Normal) (TransferFunctions) module Interprocedural = AbstractInterpreter.Interprocedural (Summary) (* a results table is a Map where a key is an a procedure environment, i.e., something of type Idenv.t * Tenv.t * Procname.t * Procdesc.t *) module ResultsTableType = Caml.Map.Make (struct type t = Idenv.t * Tenv.t * Procname.t * Procdesc.t let compare (_, _, pn1, _) (_,_,pn2,_) = Procname.compare pn1 pn2 end) (* we want to consider Builder classes and other safe immutablility-ensuring patterns as thread-safe. we are overly friendly about this for now; any class whose name ends with `Builder` is assumed to be thread-safe. in the future, we can ask for builder classes to be annotated with @Builder and verify that annotated classes satisfy the expected invariants. *) let is_builder_class class_name = String.is_suffix ~suffix:"Builder" class_name (* similarly, we assume that immutable classes safely encapsulate their state *) let is_immutable_collection_class class_name tenv = let immutable_collections = [ "com.google.common.collect.ImmutableCollection"; "com.google.common.collect.ImmutableMap"; "com.google.common.collect.ImmutableTable"; ] in PatternMatch.supertype_exists tenv (fun typename _ -> List.mem ~equal:String.equal immutable_collections (Typename.name typename)) class_name let is_call_to_builder_class_method = function | Procname.Java java_pname -> is_builder_class (Procname.java_get_class_name java_pname) | _ -> false let is_call_to_immutable_collection_method tenv = function | Procname.Java java_pname -> is_immutable_collection_class (Procname.java_get_class_type_name java_pname) tenv | _ -> false (* Methods in @ThreadConfined classes and methods annotated with @ThreadConfied are assumed to all run on the same thread. For the moment we won't warn on accesses resulting from use of such methods at all. In future we should account for races between these methods and methods from completely different classes that don't necessarily run on the same thread as the confined object. *) let is_thread_confined_method tenv pdesc = Annotations.pdesc_return_annot_ends_with pdesc Annotations.thread_confined || PatternMatch.check_current_class_attributes Annotations.ia_is_thread_confined tenv (Procdesc.get_proc_name pdesc) (* we don't want to warn on methods that run on the UI thread because they should always be single-threaded *) let runs_on_ui_thread proc_desc = (* assume that methods annotated with @UiThread, @OnEvent, @OnBind, @OnMount, @OnUnbind, @OnUnmount always run on the UI thread *) Annotations.pdesc_has_return_annot proc_desc (fun annot -> Annotations.ia_is_ui_thread annot || Annotations.ia_is_on_bind annot || Annotations.ia_is_on_event annot || Annotations.ia_is_on_mount annot || Annotations.ia_is_on_unbind annot || Annotations.ia_is_on_unmount annot) let is_assumed_thread_safe pdesc = Annotations.pdesc_return_annot_ends_with pdesc Annotations.assume_thread_safe (* return true if we should compute a summary for the procedure. if this returns false, we won't analyze the procedure or report any warnings on it *) (* note: in the future, we will want to analyze the procedures in all of these cases in order to find more bugs. this is just a temporary measure to avoid obvious false positives *) let should_analyze_proc pdesc tenv = let pn = Procdesc.get_proc_name pdesc in not (Procname.is_class_initializer pn) && not (FbThreadSafety.is_logging_method pn) && not (is_call_to_builder_class_method pn) && not (is_call_to_immutable_collection_method tenv pn) && not (runs_on_ui_thread pdesc) && not (is_thread_confined_method tenv pdesc) && not (is_assumed_thread_safe pdesc) (* return true if we should report on unprotected accesses during the procedure *) let should_report_on_proc (_, _, proc_name, proc_desc) = not (Procname.java_is_autogen_method proc_name) && Procdesc.get_access proc_desc <> PredSymb.Private && not (Annotations.pdesc_return_annot_ends_with proc_desc Annotations.visibleForTesting) (* creates a map from proc_envs to postconditions *) let make_results_table get_proc_desc file_env = (* make a Map sending each element e of list l to (f e) *) let map_post_computation_over_procs f l = IList.fold_left (fun m p -> ResultsTableType.add p (f p) m ) ResultsTableType.empty l in let is_initializer tenv proc_name = Procname.is_constructor proc_name || FbThreadSafety.is_custom_init tenv proc_name in let compute_post_for_procedure = (* takes proc_env as arg *) fun (idenv, tenv, proc_name, proc_desc) -> let open ThreadSafetyDomain in let has_lock = false in let ret_is_owned = false in let empty = has_lock, PathDomain.empty, ConditionalWritesDomain.empty, PathDomain.empty, ret_is_owned in (* convert the abstract state to a summary by dropping the id map *) let compute_post ({ ProcData.pdesc; tenv; extras; } as proc_data) = if should_analyze_proc pdesc tenv then begin if not (Procdesc.did_preanalysis pdesc) then Preanal.do_liveness pdesc tenv; let initial = if is_initializer tenv (Procdesc.get_proc_name pdesc) then (* express that the constructor owns [this] *) match FormalMap.get_formal_base 0 extras with | Some base -> let attribute_map = AttributeMapDomain.add_attribute (base, []) Owned ThreadSafetyDomain.empty.attribute_map in { ThreadSafetyDomain.empty with attribute_map; } | None -> ThreadSafetyDomain.empty else ThreadSafetyDomain.empty in match Analyzer.compute_post proc_data ~initial with | Some { locks; reads; conditional_writes; unconditional_writes; attribute_map; } -> let return_var_ap = AccessPath.of_pvar (Pvar.get_ret_pvar (Procdesc.get_proc_name pdesc)) (Procdesc.get_ret_type pdesc) in let return_is_owned = AttributeMapDomain.has_attribute return_var_ap Owned attribute_map in Some (locks, reads, conditional_writes, unconditional_writes, return_is_owned) | None -> None end else Some empty in let callback_arg = let get_procs_in_file _ = [] in { Callbacks.get_proc_desc; get_procs_in_file; idenv; tenv; proc_name; proc_desc } in match Interprocedural.compute_and_store_post ~compute_post ~make_extras:FormalMap.make callback_arg with | Some post -> post | None -> empty in map_post_computation_over_procs compute_post_for_procedure file_env let get_current_class_and_threadsafe_superclasses tenv pname = match pname with | Procname.Java java_pname -> let current_class = Procname.java_get_class_type_name java_pname in let thread_safe_annotated_classes = PatternMatch.find_superclasses_with_attributes Annotations.ia_is_thread_safe tenv current_class in Some (current_class,thread_safe_annotated_classes) | _ -> None (*shouldn't happen*) (** The addendum message says that a superclass is marked @ThreadSafe, when the current class is not so marked*) let calculate_addendum_message tenv pname = match get_current_class_and_threadsafe_superclasses tenv pname with | Some (current_class,thread_safe_annotated_classes) -> if not (List.mem ~equal:Typename.equal thread_safe_annotated_classes current_class) then match thread_safe_annotated_classes with | hd::_ -> F.asprintf "\n Note: Superclass %a is marked @ThreadSafe." Typename.pp hd | [] -> "" else "" | _ -> "" let combine_conditional_unconditional_writes conditional_writes unconditional_writes = let open ThreadSafetyDomain in ConditionalWritesDomain.fold (fun _ writes acc -> PathDomain.join writes acc) conditional_writes unconditional_writes let report_thread_safety_violations ( _, tenv, pname, pdesc) trace = let open ThreadSafetyDomain in let trace_of_pname callee_pname = match Summary.read_summary pdesc callee_pname with | Some (_, _, conditional_writes, unconditional_writes, _) -> combine_conditional_unconditional_writes conditional_writes unconditional_writes | _ -> PathDomain.empty in let report_one_path ((_, sinks) as path) = let pp_accesses fmt sink = let _, accesses = PathDomain.Sink.kind sink in AccessPath.pp_access_list fmt accesses in let initial_sink, _ = IList.hd (IList.rev sinks) in let final_sink, _ = IList.hd sinks in let initial_sink_site = PathDomain.Sink.call_site initial_sink in let final_sink_site = PathDomain.Sink.call_site final_sink in let desc_of_sink sink = if CallSite.equal (PathDomain.Sink.call_site sink) final_sink_site then Format.asprintf "access to %a" pp_accesses sink else Format.asprintf "call to %a" Procname.pp (CallSite.pname (PathDomain.Sink.call_site sink)) in let loc = CallSite.loc (PathDomain.Sink.call_site initial_sink) in let ltr = PathDomain.to_sink_loc_trace ~desc_of_sink path in let msg = Localise.to_string Localise.thread_safety_violation in let description = Format.asprintf "Public method %a%s writes to field %a outside of synchronization.%s" Procname.pp pname (if CallSite.equal final_sink_site initial_sink_site then "" else " indirectly") pp_accesses final_sink (calculate_addendum_message tenv pname) in let exn = Exceptions.Checkers (msg, Localise.verbatim_desc description) in Reporting.log_error pname ~loc ~ltr exn in IList.iter report_one_path (PathDomain.get_reportable_sink_paths trace ~trace_of_pname) (* Currently we analyze if there is an @ThreadSafe annotation on at least one of the classes in a file. This might be tightened in future or even broadened in future based on other criteria *) let should_report_on_file file_env = let current_class_or_super_marked_threadsafe = fun (_, tenv, pname, _) -> match get_current_class_and_threadsafe_superclasses tenv pname with | Some (_, thread_safe_annotated_classes) -> not (List.is_empty thread_safe_annotated_classes) | _ -> false in let current_class_marked_not_threadsafe = fun (_, tenv, pname, _) -> PatternMatch.check_current_class_attributes Annotations.ia_is_not_thread_safe tenv pname in not (List.exists ~f:current_class_marked_not_threadsafe file_env) && List.exists ~f:current_class_or_super_marked_threadsafe file_env (* For now, just checks if there is one active element amongst the posts of the analyzed methods. This indicates that the method races with itself. To be refined later. *) let process_results_table file_env tab = let should_report_on_all_procs = should_report_on_file file_env in (* TODO (t15588153): clean this up *) let is_thread_safe_method pdesc tenv = let overrides_thread_safe_method pname tenv = PatternMatch.override_exists (fun pn -> Annotations.pname_has_return_annot pn ~attrs_of_pname:Specs.proc_resolve_attributes Annotations.ia_is_thread_safe_method) tenv pname in Annotations.pdesc_return_annot_ends_with pdesc Annotations.thread_safe_method || overrides_thread_safe_method (Procdesc.get_proc_name pdesc) tenv in let should_report ((_, tenv, _, pdesc) as proc_env) = (should_report_on_all_procs || is_thread_safe_method pdesc tenv) && should_report_on_proc proc_env in ResultsTableType.iter (* report errors for each method *) (fun proc_env (_, _, conditional_writes, unconditional_writes, _) -> if should_report proc_env then combine_conditional_unconditional_writes conditional_writes unconditional_writes |> report_thread_safety_violations proc_env) tab (*This is a "cluster checker" *) (*Gathers results by analyzing all the methods in a file, then post-processes the results to check (approximation of) thread safety *) (* file_env: (Idenv.t * Tenv.t * Procname.t * Procdesc.t) list *) let file_analysis _ _ get_procdesc file_env = process_results_table file_env (make_results_table get_procdesc file_env)