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.

473 lines
20 KiB

(*
* Copyright (c) 2016 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
open! IStd
module F = Format
module L = Logging
module 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 = ProcData.no_extras
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 add_path_to_state exp typ loc path_state id_map owned 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
let is_thread_confined (_, accesses) = match IList.rev accesses with
| AccessPath.FieldAccess (fieldname, Typ.Tstruct typename) :: _ ->
begin
match Tenv.lookup tenv typename with
| Some struct_typ ->
Annotations.field_has_annot fieldname struct_typ Annotations.ia_is_thread_confined
| None ->
false
end
| _ ->
false in
let f_resolve_id = resolve_id id_map in
IList.fold_left
(fun acc rawpath ->
if not (ThreadSafetyDomain.OwnershipDomain.mem (truncate rawpath) owned) &&
not (is_thread_confined rawpath)
then
ThreadSafetyDomain.PathDomain.add_sink (ThreadSafetyDomain.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 { ThreadSafetyDomain.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
let exec_instr (astate : Domain.astate) { ProcData.pdesc; tenv; } _ =
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.Map", ("clear" | "put" | "putAll" | "remove") -> true
| _ -> false in
let is_threadsafe_collection typename _ = match Typename.name typename with
| "java.util.concurrent.ConcurrentMap" -> 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 : ThreadSafetyDomain.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.owned tenv in
{ astate with unconditional_writes; } in
let is_unprotected is_locked =
not is_locked && not (Procdesc.is_java_synchronized pdesc) in
let f_resolve_id = resolve_id astate.id_map 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 owned = ThreadSafetyDomain.OwnershipDomain.add lhs_access_path astate.owned in
{ astate with owned; }
| None ->
astate
end
| Sil.Call (_, Const (Cfun pn), actuals, loc, _) ->
begin
(* assuming that modeled procedures do not have useful summaries *)
match get_lock_model pn with
| Lock ->
{ astate with locks = true; }
| Unlock ->
{ astate with locks = false; }
| NoEffect ->
if is_unprotected astate.locks && is_container_write pn tenv
then
match actuals with
| (receiver_exp, receiver_typ) :: _ ->
add_container_write pn loc receiver_exp receiver_typ astate
| [] ->
failwithf
"Call to %a is marked as a container write, but has no receiver"
Procname.pp pn
else
begin
match Summary.read_summary pdesc pn with
| Some (callee_locks, callee_reads, _, callee_unconditional_writes) ->
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 pn loc in
let reads =
ThreadSafetyDomain.PathDomain.with_callsite callee_reads call_site
|> ThreadSafetyDomain.PathDomain.join astate.reads in
let unconditional_writes =
ThreadSafetyDomain.PathDomain.with_callsite
callee_unconditional_writes
call_site
|> ThreadSafetyDomain.PathDomain.join astate.unconditional_writes in
{ astate with reads; unconditional_writes; }
else
astate in
{ astate' with locks = locks'; }
| None ->
astate
end
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 unconditional_writes =
match lhs_exp with
| Lfield ( _, _, typ)
when is_unprotected astate.locks -> (* abstracts no lock being held *)
add_path_to_state
lhs_exp typ loc astate.unconditional_writes astate.id_map astate.owned tenv
| _ ->
astate.unconditional_writes in
(* if rhs is owned, propagate ownership to lhs. otherwise, remove lhs from ownerhsip set
(since it may have previously held an owned memory loc and is now being reassigned *)
let owned =
match AccessPath.of_lhs_exp lhs_exp lhs_typ ~f_resolve_id,
AccessPath.of_lhs_exp rhs_exp lhs_typ ~f_resolve_id with
| Some lhs_access_path, Some rhs_access_path ->
if ThreadSafetyDomain.OwnershipDomain.mem rhs_access_path astate.owned
then ThreadSafetyDomain.OwnershipDomain.add lhs_access_path astate.owned
else ThreadSafetyDomain.OwnershipDomain.remove lhs_access_path astate.owned
| Some lhs_access_path, None ->
ThreadSafetyDomain.OwnershipDomain.remove lhs_access_path astate.owned
| _ ->
astate.owned in
{ astate with unconditional_writes; owned; }
| 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.owned tenv
| _ ->
astate.reads in
(* if rhs is owned, propagate ownership to lhs *)
let owned =
match AccessPath.of_lhs_exp rhs_exp rhs_typ ~f_resolve_id with
| Some rhs_access_path
when ThreadSafetyDomain.OwnershipDomain.mem rhs_access_path astate.owned ->
ThreadSafetyDomain.OwnershipDomain.add (AccessPath.of_id lhs_id rhs_typ) astate.owned
| _ ->
astate.owned in
{ astate with Domain.reads; id_map; owned; }
| 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)
(Scheduler.ReversePostorder)
(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 _ -> IList.mem (=) (Typename.name typename) immutable_collections) 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
let is_initializer tenv proc_name =
Procname.is_constructor proc_name ||
Procname.is_class_initializer proc_name ||
FbThreadSafety.is_custom_init tenv proc_name
let is_annotated f_annot pdesc =
let annotated_signature = Annotations.get_annotated_signature (Procdesc.get_attributes pdesc) in
let ret_annotation, _ = annotated_signature.Annotations.ret in
f_annot ret_annotation
(* 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 =
is_annotated Annotations.ia_is_thread_confined pdesc ||
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 or @OnEvent(SomeEvent.class) always run on the UI
thread *)
is_annotated
(fun annot -> Annotations.ia_is_ui_thread annot || Annotations.ia_is_on_event annot)
proc_desc
(* 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 (is_initializer tenv 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)
(* return true if we should report on unprotected accesses during the procedure *)
let should_report_on_proc (_, tenv, proc_name, proc_desc) =
not (is_initializer tenv proc_name) &&
not (Procname.java_is_autogen_method proc_name) &&
Procdesc.get_access proc_desc <> PredSymb.Private
(* 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 compute_post_for_procedure = (* takes proc_env as arg *)
fun (idenv, tenv, proc_name, proc_desc) ->
let empty =
false,
ThreadSafetyDomain.PathDomain.empty,
ThreadSafetyDomain.ConditionalWritesDomain.empty,
ThreadSafetyDomain.PathDomain.empty in
(* convert the abstract state to a summary by dropping the id map *)
let compute_post ({ ProcData.pdesc; tenv; } as proc_data) =
if should_analyze_proc pdesc tenv
then
begin
if not (Procdesc.did_preanalysis pdesc) then Preanal.do_liveness pdesc tenv;
match Analyzer.compute_post proc_data ~initial:ThreadSafetyDomain.empty with
| Some { locks; reads; conditional_writes; unconditional_writes; } ->
Some (locks, reads, conditional_writes, unconditional_writes)
| None ->
None
end
else
Some empty in
let callback_arg =
{Callbacks.get_proc_desc; get_procs_in_file = (fun _ -> []);
idenv; tenv; proc_name; proc_desc} in
match
Interprocedural.compute_and_store_post
~compute_post
~make_extras:ProcData.make_empty_extras
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 (IList.mem Typename.equal current_class thread_safe_annotated_classes) then
match thread_safe_annotated_classes with
| hd::_ -> F.asprintf "\n Note: Superclass %a is marked @ThreadSafe." Typename.pp hd
| [] -> ""
else ""
| _ -> ""
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 (_, _, _, unconditional_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 (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 (IList.exists current_class_marked_not_threadsafe file_env) &&
IList.exists 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
let should_report ((_, _, _, pdesc) as proc_env) =
(should_report_on_all_procs || is_annotated Annotations.ia_is_thread_safe_method pdesc)
&& should_report_on_proc proc_env in
ResultsTableType.iter (* report errors for each method *)
(fun proc_env (_, _, _, unconditional_writes) ->
if should_report proc_env then report_thread_safety_violations proc_env unconditional_writes)
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)