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.

627 lines
27 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
(** Create a taint analysis from a specification *)
module Make (TaintSpecification : TaintSpec.S) = struct
module TraceDomain = TaintSpecification.Trace
module TaintDomain = TaintSpecification.AccessTree
module IdMapDomain = IdAccessPathMapDomain
module Summary = Summary.Make(struct
type payload = QuandarySummary.t
let update_payload quandary_payload (summary : Specs.summary) =
{ summary with payload = { summary.payload with quandary = Some quandary_payload }}
let read_payload (summary : Specs.summary) =
summary.payload.quandary
end)
module Domain = struct
type astate =
{
access_tree : TaintDomain.astate; (* mapping of access paths to trace sets *)
id_map : IdMapDomain.astate; (* mapping of id's to access paths for normalization *)
}
let empty =
let access_tree = TaintDomain.empty in
let id_map = IdMapDomain.empty in
{ access_tree; id_map; }
let (<=) ~lhs ~rhs =
if phys_equal lhs rhs
then true
else
TaintDomain.(<=) ~lhs:lhs.access_tree ~rhs:rhs.access_tree &&
IdMapDomain.(<=) ~lhs:lhs.id_map ~rhs:rhs.id_map
let join astate1 astate2 =
if phys_equal astate1 astate2
then astate1
else
let access_tree = TaintDomain.join astate1.access_tree astate2.access_tree in
let id_map = IdMapDomain.join astate1.id_map astate2.id_map in
{ access_tree; id_map; }
let widen ~prev ~next ~num_iters =
if phys_equal prev next
then prev
else
let access_tree =
TaintDomain.widen ~prev:prev.access_tree ~next:next.access_tree ~num_iters in
let id_map = IdMapDomain.widen ~prev:prev.id_map ~next:next.id_map ~num_iters in
{ access_tree; id_map; }
let pp fmt { access_tree; id_map; } =
F.fprintf fmt "(%a, %a)" TaintDomain.pp access_tree IdMapDomain.pp id_map
end
let is_global (var, _) = match var with
| Var.ProgramVar pvar -> Pvar.is_global pvar
| Var.LogicalVar _ -> false
let is_return (var, _) = match var with
| Var.ProgramVar pvar -> Pvar.is_return pvar
| Var.LogicalVar _ -> false
let is_footprint (var, _) = match var with
| Var.ProgramVar _ -> false
| Var.LogicalVar id -> Ident.is_footprint id
let make_footprint_var formal_index =
Var.of_id (Ident.create_footprint Ident.name_spec formal_index)
let make_footprint_access_path formal_index access_path =
AccessPath.with_base_var (make_footprint_var formal_index) access_path
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = Domain
type extras = FormalMap.t
let resolve_id id_map id =
try Some (IdMapDomain.find id id_map)
with Not_found -> None
(* get the node associated with [access_path] in [access_tree] *)
let access_path_get_node access_path access_tree (proc_data : FormalMap.t ProcData.t) =
match TaintDomain.get_node access_path access_tree with
| Some _ as node_opt ->
node_opt
| None ->
let make_footprint_trace footprint_ap =
let trace =
TraceDomain.of_source
(TraceDomain.Source.make_footprint footprint_ap proc_data.pdesc) in
Some (TaintDomain.make_normal_leaf trace) in
let root, _ = AccessPath.extract access_path in
match FormalMap.get_formal_index root proc_data.extras with
| Some formal_index ->
make_footprint_trace (make_footprint_access_path formal_index access_path)
| None ->
if is_global root
then make_footprint_trace access_path
else None
(* get the trace associated with [access_path] in [access_tree]. *)
let access_path_get_trace access_path access_tree proc_data =
match access_path_get_node access_path access_tree proc_data with
| Some (trace, _) -> trace
| None -> TraceDomain.empty
(* get the node associated with [exp] in [access_tree] *)
let exp_get_node ?(abstracted=false) exp typ { Domain.access_tree; id_map; } proc_data =
let f_resolve_id = resolve_id id_map in
match AccessPath.of_lhs_exp exp typ ~f_resolve_id with
| Some raw_access_path ->
let access_path =
if abstracted
then AccessPath.Abstracted raw_access_path
else AccessPath.Exact raw_access_path in
access_path_get_node access_path access_tree proc_data
| None ->
(* can't make an access path from [exp] *)
None
let analyze_assignment lhs_access_path rhs_exp rhs_typ astate proc_data =
let rhs_node =
match exp_get_node rhs_exp rhs_typ astate proc_data with
| Some node -> node
| None -> TaintDomain.empty_node in
let access_tree = TaintDomain.add_node lhs_access_path rhs_node astate.Domain.access_tree in
{ astate with Domain.access_tree; }
let analyze_id_assignment lhs_id rhs_exp rhs_typ ({ Domain.id_map; } as astate) =
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 ->
let id_map' = IdMapDomain.add lhs_id rhs_access_path id_map in
{ astate with Domain.id_map = id_map'; }
| None ->
astate
let add_source source ret_id ret_typ access_tree =
let trace = TraceDomain.of_source source in
let id_ap = AccessPath.Exact (AccessPath.of_id ret_id ret_typ) in
TaintDomain.add_trace id_ap trace access_tree
(** log any new reportable source-sink flows in [trace] *)
let report_trace trace cur_site (proc_data : FormalMap.t ProcData.t) =
let trace_of_pname pname =
if Typ.Procname.equal pname (Procdesc.get_proc_name proc_data.pdesc)
then
(* read_summary will trigger ondemand analysis of the current proc. we don't want that. *)
TraceDomain.empty
else
match Summary.read_summary proc_data.pdesc pname with
| Some summary ->
TaintDomain.trace_fold
(fun acc _ trace -> TraceDomain.join trace acc)
(TaintSpecification.of_summary_access_tree summary)
TraceDomain.empty
| None ->
TraceDomain.empty in
let pp_path_short fmt (_, sources_passthroughs, sinks_passthroughs) =
let original_source = fst (List.hd_exn sources_passthroughs) in
let final_sink = fst (List.hd_exn sinks_passthroughs) in
F.fprintf
fmt
"%a -> %a"
TraceDomain.Source.pp original_source
TraceDomain.Sink.pp final_sink in
let report_error path =
let caller_pname = Procdesc.get_proc_name proc_data.pdesc in
let msg = Localise.to_issue_id Localise.quandary_taint_error in
let trace_str = F.asprintf "%a" pp_path_short path in
let ltr = TraceDomain.to_loc_trace path in
let exn = Exceptions.Checkers (msg, Localise.verbatim_desc trace_str) in
Reporting.log_error caller_pname ~loc:(CallSite.loc cur_site) ~ltr exn in
List.iter ~f:report_error (TraceDomain.get_reportable_paths ~cur_site trace ~trace_of_pname)
let add_sinks sinks actuals ({ Domain.access_tree; id_map; } as astate) proc_data callee_site =
let f_resolve_id = resolve_id id_map in
(* add [sink] to the trace associated with the [formal_index]th actual *)
let add_sink_to_actual access_tree_acc (sink_param : TraceDomain.Sink.parameter) =
let actual_exp, actual_typ = List.nth_exn actuals sink_param.index in
match AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id with
| Some actual_ap_raw ->
let actual_ap =
let is_array_typ = match actual_typ with
| Typ.Tptr (Tarray _, _) (* T* [] (Java-style) *)
| Tptr (Tptr _, _) (* T** (C/C++ style 1) *)
| Tarray _ (* T[] C/C++ style 2 *) ->
true
| _ ->
false in
(* conisder any sources that are reachable from an array *)
if sink_param.report_reachable || is_array_typ
then AccessPath.Abstracted actual_ap_raw
else AccessPath.Exact actual_ap_raw in
begin
match access_path_get_node actual_ap access_tree_acc proc_data with
| Some (actual_trace, _) ->
let actual_trace' = TraceDomain.add_sink sink_param.sink actual_trace in
report_trace actual_trace' callee_site proc_data;
TaintDomain.add_trace actual_ap actual_trace' access_tree_acc
| None ->
access_tree_acc
end
| None ->
access_tree_acc in
let access_tree' = List.fold ~f:add_sink_to_actual ~init:access_tree sinks in
{ astate with Domain.access_tree = access_tree'; }
let apply_summary
ret_opt
actuals
summary
(astate_in : Domain.astate)
(proc_data : FormalMap.t ProcData.t)
callee_site =
let caller_access_tree = astate_in.access_tree in
let get_caller_ap formal_ap =
let apply_return ret_ap = match ret_opt with
| Some (ret_id, _) -> AccessPath.with_base_var (Var.of_id ret_id) ret_ap
| None -> failwith "Have summary for retval, but no ret id to bind it to!" in
let get_actual_ap formal_index =
let f_resolve_id = resolve_id astate_in.id_map in
List.nth actuals formal_index |>
Option.value_map
~f:(fun (actual_exp, actual_typ) ->
AccessPath.of_lhs_exp actual_exp actual_typ ~f_resolve_id )
~default:None in
let project ~formal_ap ~actual_ap =
let projected_ap = AccessPath.append actual_ap (snd (AccessPath.extract formal_ap)) in
if AccessPath.is_exact formal_ap
then AccessPath.Exact projected_ap
else AccessPath.Abstracted projected_ap in
let base_var, _ = fst (AccessPath.extract formal_ap) in
match base_var with
| Var.ProgramVar pvar ->
if Pvar.is_return pvar
then Some (apply_return formal_ap)
else Some formal_ap
| Var.LogicalVar id when Ident.is_footprint id ->
begin
(* summaries store the index of the formal parameter in the ident stamp *)
match get_actual_ap (Ident.get_stamp id) with
| Some actual_ap ->
let projected_ap = project ~formal_ap ~actual_ap in
Some projected_ap
| None ->
None
end
| _ ->
None in
let get_caller_ap_node_opt ap access_tree =
let get_caller_node caller_ap =
let caller_node_opt = access_path_get_node caller_ap access_tree proc_data in
let caller_node = Option.value ~default:TaintDomain.empty_node caller_node_opt in
caller_ap, caller_node in
Option.map (get_caller_ap ap) ~f:get_caller_node in
let replace_footprint_sources callee_trace caller_trace access_tree =
let replace_footprint_source source acc =
match TraceDomain.Source.get_footprint_access_path source with
| Some footprint_access_path ->
begin
match get_caller_ap_node_opt footprint_access_path access_tree with
| Some (_, (caller_ap_trace, _)) -> TraceDomain.join caller_ap_trace acc
| None -> acc
end
| None ->
acc in
TraceDomain.Sources.fold
replace_footprint_source (TraceDomain.sources callee_trace) caller_trace in
let instantiate_and_report callee_trace caller_trace access_tree =
let caller_trace' = replace_footprint_sources callee_trace caller_trace access_tree in
let appended_trace = TraceDomain.append caller_trace' callee_trace callee_site in
report_trace appended_trace callee_site proc_data;
appended_trace in
let add_to_caller_tree access_tree_acc callee_ap callee_trace =
match get_caller_ap_node_opt callee_ap access_tree_acc with
| Some (caller_ap, (caller_trace, caller_tree)) ->
let trace = instantiate_and_report callee_trace caller_trace access_tree_acc in
TaintDomain.add_node caller_ap (trace, caller_tree) access_tree_acc
| None ->
ignore (instantiate_and_report callee_trace TraceDomain.empty access_tree_acc);
access_tree_acc in
let access_tree =
TaintDomain.trace_fold
add_to_caller_tree
(TaintSpecification.of_summary_access_tree summary)
caller_access_tree in
{ astate_in with access_tree; }
let exec_instr (astate : Domain.astate) (proc_data : FormalMap.t ProcData.t) _ instr =
let f_resolve_id = resolve_id astate.id_map in
match instr with
| Sil.Load (lhs_id, rhs_exp, rhs_typ, _) ->
analyze_id_assignment (Var.of_id lhs_id) rhs_exp rhs_typ astate
| Sil.Store (Exp.Lvar lhs_pvar, lhs_typ, rhs_exp, _) when Pvar.is_ssa_frontend_tmp lhs_pvar ->
analyze_id_assignment (Var.of_pvar lhs_pvar) rhs_exp lhs_typ astate
| Sil.Store (Exp.Lvar lhs_pvar, _, Exp.Exn _, _) when Pvar.is_return lhs_pvar ->
(* the Java frontend translates `throw Exception` as `return Exception`, which is a bit
wonky. this translation causes problems for us in computing a summary when an
exception is "returned" from a void function. skip code like this for now
(fix via t14159157 later *)
astate
| Sil.Store (Exp.Lvar lhs_pvar, _, rhs_exp, _)
when Pvar.is_return lhs_pvar && Exp.is_null_literal rhs_exp &&
Typ.equal Tvoid (Procdesc.get_ret_type proc_data.pdesc) ->
(* similar to the case above; the Java frontend translates "return no exception" as
`return null` in a void function *)
astate
| Sil.Store (lhs_exp, lhs_typ, rhs_exp, loc) ->
let lhs_access_path =
match AccessPath.of_lhs_exp lhs_exp lhs_typ ~f_resolve_id with
| Some access_path ->
access_path
| None ->
failwithf
"Assignment to unexpected lhs expression %a in proc %a at loc %a"
Exp.pp lhs_exp
Typ.Procname.pp (Procdesc.get_proc_name (proc_data.pdesc))
Location.pp loc in
let astate' =
analyze_assignment
(AccessPath.Exact lhs_access_path) rhs_exp lhs_typ astate proc_data in
begin
(* direct `exp = id` assignments are treated specially; we update the id map too. this
is so future reads of `exp` will get the subtree associated with `id` (needed to
handle the `id = foo(); exp = id case` and similar). *)
match rhs_exp with
| Exp.Var rhs_id ->
let existing_accesses =
try snd (IdMapDomain.find (Var.of_id rhs_id) astate'.Domain.id_map)
with Not_found -> [] in
let lhs_ap' = AccessPath.append lhs_access_path existing_accesses in
let id_map' = IdMapDomain.add (Var.of_id rhs_id) lhs_ap' astate'.Domain.id_map in
{ astate' with Domain.id_map = id_map'; }
| _ ->
astate'
end
| Sil.Call (Some (ret_id, _), Const (Cfun callee_pname), args, loc, _)
when BuiltinDecl.is_declared callee_pname ->
if Typ.Procname.equal callee_pname BuiltinDecl.__cast
then
match args with
| (cast_target, cast_typ) :: _ ->
analyze_id_assignment (Var.of_id ret_id) cast_target cast_typ astate
| _ ->
failwithf
"Unexpected cast %a in procedure %a at line %a"
(Sil.pp_instr Pp.text) instr
Typ.Procname.pp (Procdesc.get_proc_name (proc_data.pdesc))
Location.pp loc
else
astate
| Sil.Call (ret, Const (Cfun called_pname), actuals, callee_loc, call_flags) ->
let handle_unknown_call callee_pname astate =
let is_variadic = match callee_pname with
| Typ.Procname.Java pname ->
begin
match List.rev (Typ.Procname.java_get_parameters pname) with
| (_, "java.lang.Object[]") :: _ -> true
| _ -> false
end
| _ -> false in
let should_taint_typ typ = is_variadic || TaintSpecification.is_taintable_type typ in
let exp_join_traces trace_acc (exp, typ) =
match exp_get_node ~abstracted:true exp typ astate proc_data with
| Some (trace, _) -> TraceDomain.join trace trace_acc
| None -> trace_acc in
let propagate_to_access_path access_path actuals (astate : Domain.astate) =
let initial_trace = access_path_get_trace access_path astate.access_tree proc_data in
let trace_with_propagation =
List.fold ~f:exp_join_traces ~init:initial_trace actuals in
let filtered_sources =
TraceDomain.Sources.filter (fun source ->
match TraceDomain.Source.get_footprint_access_path source with
| Some access_path ->
Option.exists
(AccessPath.Raw.get_typ (AccessPath.extract access_path) proc_data.tenv)
~f:should_taint_typ
| None ->
true)
(TraceDomain.sources trace_with_propagation) in
if TraceDomain.Sources.is_empty filtered_sources
then
astate
else
let trace' = TraceDomain.update_sources trace_with_propagation filtered_sources in
let access_tree =
TaintDomain.add_trace access_path trace' astate.access_tree in
{ astate with Domain.access_tree; } in
let handle_unknown_call_ astate_acc propagation =
match propagation, actuals, ret with
| _, [], _ ->
astate_acc
| TaintSpec.Propagate_to_return, actuals, Some (ret_id, ret_typ) ->
let ret_ap = AccessPath.Exact (AccessPath.of_id ret_id ret_typ) in
propagate_to_access_path ret_ap actuals astate_acc
| TaintSpec.Propagate_to_receiver,
(receiver_exp, receiver_typ) :: (_ :: _ as other_actuals),
_ ->
begin
match AccessPath.of_lhs_exp receiver_exp receiver_typ ~f_resolve_id with
| Some ap ->
propagate_to_access_path (AccessPath.Exact ap) other_actuals astate_acc
| None ->
(* this can happen when (for example) the receiver is a string literal *)
astate_acc
end
| _ ->
astate_acc in
let propagations =
TaintSpecification.handle_unknown_call
callee_pname
(Option.map ~f:snd ret)
actuals
proc_data.tenv in
List.fold ~f:handle_unknown_call_ ~init:astate propagations in
let analyze_call astate_acc callee_pname =
let call_site = CallSite.make callee_pname callee_loc in
let sinks = TraceDomain.Sink.get call_site actuals proc_data.ProcData.tenv in
let astate_with_sink = match sinks with
| [] -> astate
| sinks -> add_sinks sinks actuals astate proc_data call_site in
let source = TraceDomain.Source.get call_site proc_data.tenv in
let astate_with_source =
match source, ret with
| Some source, Some (ret_id, ret_typ) ->
let access_tree = add_source source ret_id ret_typ astate_with_sink.access_tree in
{ astate_with_sink with access_tree; }
| Some _, None ->
L.err
"Warning: %a is marked as a source, but has no return value"
Typ.Procname.pp callee_pname;
astate_with_sink
| None, _ ->
astate_with_sink in
let astate_with_summary =
if sinks <> [] || Option.is_some source
then
(* don't use a summary for a procedure that is a direct source or sink *)
astate_with_source
else
match Summary.read_summary proc_data.pdesc callee_pname with
| Some summary ->
apply_summary ret actuals summary astate_with_source proc_data call_site
| None ->
handle_unknown_call callee_pname astate_with_source in
Domain.join astate_acc astate_with_summary in
(* highly polymorphic call sites stress reactive mode too much by using too much memory.
here, we choose an arbitrary call limit that allows us to finish the analysis in
practice. this is obviously unsound; will try to remove in the future. *)
let max_calls = 3 in
let targets =
if List.length call_flags.cf_targets <= max_calls
then
called_pname :: call_flags.cf_targets
else
begin
L.out "Skipping highly polymorphic call site for %a@." Typ.Procname.pp called_pname;
[called_pname]
end in
(* for each possible target of the call, apply the summary. join all results together *)
List.fold ~f:analyze_call ~init:Domain.empty targets
| Sil.Call _ ->
failwith "Unimp: non-pname call expressions"
| Sil.Nullify (pvar, _) ->
let id_map = IdMapDomain.remove (Var.of_pvar pvar) astate.id_map in
{ astate with id_map; }
| Sil.Remove_temps (ids, _) ->
let id_map =
List.fold
~f:(fun acc id -> IdMapDomain.remove (Var.of_id id) acc)
~init:astate.id_map
ids in
{ astate with id_map; }
| Sil.Prune _ | Abstract _ | Declare_locals _ ->
astate
end
module Analyzer = AbstractInterpreter.Make (ProcCfg.Exceptional) (TransferFunctions)
let make_summary formal_map access_tree =
(* if a trace has footprint sources, attach them to the appropriate footprint var *)
let access_tree' =
TaintDomain.fold
(fun access_tree_acc _ ((trace, _) as node) ->
if TraceDomain.Sinks.is_empty (TraceDomain.sinks trace)
then
(* if this trace has no sinks, we don't need to attach it to anything *)
access_tree_acc
else
TraceDomain.Sources.fold
(fun source acc ->
match TraceDomain.Source.get_footprint_access_path source with
| Some footprint_access_path ->
let node' =
match TaintDomain.get_node footprint_access_path acc with
| Some n -> TaintDomain.node_join node n
| None -> node in
TaintDomain.add_node footprint_access_path node' acc
| None ->
acc)
(TraceDomain.sources trace)
access_tree_acc)
access_tree
access_tree in
(* should only be used on nodes associated with a footprint base *)
let is_empty_node (trace, tree) =
TraceDomain.Sinks.is_empty (TraceDomain.sinks trace) &&
match tree with
| TaintDomain.Subtree subtree -> TaintDomain.AccessMap.is_empty subtree
| TaintDomain.Star -> true in
(* replace formal names with footprint vars for their indices. For example, for `foo(o)`, we'll
replace `o` with FP(1) *)
let with_footprint_vars =
AccessPath.BaseMap.fold
(fun base ((trace, subtree) as node) acc ->
if is_global base || is_return base
then AccessPath.BaseMap.add base node acc
else if is_footprint base
then
if is_empty_node node
then
acc
else
let node' =
if TraceDomain.Sinks.is_empty (TraceDomain.sinks trace)
then TraceDomain.empty, subtree
else node in
AccessPath.BaseMap.add base node' acc
else
match FormalMap.get_formal_index base formal_map with
| Some formal_index ->
let base' = make_footprint_var formal_index, snd base in
let joined_node =
try TaintDomain.node_join (AccessPath.BaseMap.find base' acc) node
with Not_found -> node in
if is_empty_node joined_node
then
acc
else
AccessPath.BaseMap.add base' joined_node acc
| None ->
(* base is a local var *)
acc)
access_tree'
TaintDomain.empty in
TaintSpecification.to_summary_access_tree with_footprint_vars
module Interprocedural = AbstractInterpreter.Interprocedural(Summary)
let checker ({ Callbacks.tenv; } as callback) : Specs.summary =
(* bind parameters to a trace with a tainted source (if applicable) *)
let make_initial pdesc =
let pname = Procdesc.get_proc_name pdesc in
let access_tree =
List.fold ~f:(fun acc (name, typ, taint_opt) ->
match taint_opt with
| Some source ->
let base_ap = AccessPath.Exact (AccessPath.of_pvar (Pvar.mk name pname) typ) in
TaintDomain.add_trace base_ap (TraceDomain.of_source source) acc
| None ->
acc)
~init:TaintDomain.empty
(TraceDomain.Source.get_tainted_formals pdesc tenv) in
if TaintDomain.BaseMap.is_empty access_tree
then Domain.empty
else { Domain.empty with Domain.access_tree; } in
let compute_post (proc_data : FormalMap.t ProcData.t) =
if not (Procdesc.did_preanalysis proc_data.pdesc)
then
begin
Preanal.do_liveness proc_data.pdesc proc_data.tenv;
Preanal.do_dynamic_dispatch proc_data.pdesc (Cg.create SourceFile.invalid) proc_data.tenv;
end;
let initial = make_initial proc_data.pdesc in
match Analyzer.compute_post proc_data ~initial with
| Some { access_tree; } ->
Some (make_summary proc_data.extras access_tree)
| None ->
if Procdesc.Node.get_succs (Procdesc.get_start_node proc_data.pdesc) <> []
then failwith "Couldn't compute post"
else None in
let make_extras = FormalMap.make in
Interprocedural.compute_and_store_post ~compute_post ~make_extras callback
end