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.

179 lines
6.7 KiB

(*
* Copyright (c) 2017 - 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 Domain = LithoDomain
module Summary = Summary.Make (struct
type payload = Domain.astate
let update_payload astate (summary: Specs.summary) =
{summary with payload= {summary.payload with litho= Some astate}}
let read_payload (summary: Specs.summary) = summary.payload.litho
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = Domain
type extras = ProcData.no_extras
let is_graphql_getter procname summary =
Option.is_none summary
(* we skip analysis of all GraphQL procs *)
&&
match procname with
| Typ.Procname.Java java_procname
-> (
PatternMatch.is_getter java_procname
&&
match Typ.Procname.Java.get_package java_procname with
| Some package ->
String.is_prefix ~prefix:"com.facebook.graphql.model" package
| None ->
false )
| _ ->
false
let apply_callee_summary summary_opt caller_pname ret_opt actuals astate =
match summary_opt with
| Some summary ->
(* TODO: append paths if the footprint access path is an actual path instead of a var *)
let f_sub {Domain.LocalAccessPath.access_path= (var, _), _} =
match Var.get_footprint_index var with
| Some footprint_index -> (
match List.nth actuals footprint_index with
| Some HilExp.AccessPath actual_access_path ->
Some (Domain.LocalAccessPath.make actual_access_path caller_pname)
| _ ->
None )
| None ->
if Var.is_return var then
match ret_opt with
| Some ret ->
Some (Domain.LocalAccessPath.make (ret, []) caller_pname)
| None ->
assert false
else None
in
Domain.substitute ~f_sub summary
| None ->
astate
let exec_instr astate (proc_data: extras ProcData.t) _ (instr: HilInstr.t) : Domain.astate =
let caller_pname = Procdesc.get_proc_name proc_data.pdesc in
match instr with
| Call
( (Some return_base as ret_opt)
, Direct callee_procname
, ((HilExp.AccessPath receiver_ap) :: _ as actuals)
, _
, _ ) ->
let summary = Summary.read_summary proc_data.pdesc callee_procname in
(* track the call if the callee is a graphql getter *or* the receiver is already tracked *)
(* TODO: we should probably track all formals as well *)
let receiver = Domain.LocalAccessPath.make receiver_ap caller_pname in
if is_graphql_getter callee_procname summary || Domain.mem receiver astate then
let receiver = Domain.LocalAccessPath.make receiver_ap caller_pname in
let return_access_path = Domain.LocalAccessPath.make (return_base, []) caller_pname in
let return_calls =
(try Domain.find return_access_path astate with Not_found -> Domain.CallSet.empty)
|> Domain.CallSet.add {receiver; procname= callee_procname}
in
Domain.add return_access_path return_calls astate
else
(* treat it like a normal call *)
apply_callee_summary summary caller_pname ret_opt actuals astate
| Call (ret_opt, Direct callee_procname, actuals, _, _) ->
let summary = Summary.read_summary proc_data.pdesc callee_procname in
apply_callee_summary summary caller_pname ret_opt actuals astate
| Assign (lhs_ap, HilExp.AccessPath rhs_ap, _)
-> (
(* creating an alias for the rhs binding; assume all reads will now occur through the
alias. this helps us keep track of chains in cases like tmp = getFoo(); x = tmp;
tmp.getBar() *)
let lhs_access_path = Domain.LocalAccessPath.make lhs_ap caller_pname in
let rhs_access_path = Domain.LocalAccessPath.make rhs_ap caller_pname in
try
let call_set = Domain.find rhs_access_path astate in
Domain.remove rhs_access_path astate |> Domain.add lhs_access_path call_set
with Not_found -> astate )
| _ ->
astate
end
module Analyzer = LowerHil.MakeAbstractInterpreter (ProcCfg.Exceptional) (TransferFunctions)
let report access_path call_chain summary =
let call_strings = List.map ~f:(Typ.Procname.to_simplified_string ~withclass:false) call_chain in
let call_string = String.concat ~sep:"." call_strings in
let message = F.asprintf "%a.%s" AccessPath.pp access_path call_string in
let exn = Exceptions.Checkers (IssueType.graphql_field_access, Localise.verbatim_desc message) in
let loc = Specs.get_loc summary in
let ltr = [Errlog.make_trace_element 0 loc message []] in
Reporting.log_error summary ~loc ~ltr exn
let unroll_call call astate summary =
let max_depth = Domain.cardinal astate in
let rec unroll_call_ ({receiver; procname}: Domain.MethodCall.t) (acc, depth) =
let acc' = procname :: acc in
let depth' = depth + 1 in
let is_cycle access_path =
(* detect direct cycles and cycles due to mutual recursion *)
Domain.LocalAccessPath.equal access_path receiver || depth' > max_depth
in
try
let calls' = Domain.find receiver astate in
Domain.CallSet.iter
(fun call ->
if not (is_cycle call.receiver) then unroll_call_ call (acc', depth')
else report receiver.access_path acc' summary )
calls'
with Not_found -> report receiver.access_path acc' summary
in
unroll_call_ call ([], 0)
let should_report proc_desc =
match Procdesc.get_proc_name proc_desc with
| Typ.Procname.Java java_pname -> (
match Typ.Procname.Java.get_method java_pname with "onCreateLayout" -> true | _ -> false )
| _ ->
false
let report_call_chains post summary =
Domain.iter
(fun _ call_set -> Domain.CallSet.iter (fun call -> unroll_call call post summary) call_set)
post
let postprocess astate proc_desc : Domain.astate =
let formal_map = FormalMap.make proc_desc in
let f_sub access_path = Domain.LocalAccessPath.to_formal_option access_path formal_map in
Domain.substitute ~f_sub astate
let checker {Callbacks.summary; proc_desc; tenv} =
let proc_data = ProcData.make_default proc_desc tenv in
match Analyzer.compute_post proc_data ~initial:Domain.empty with
| Some post ->
if should_report proc_desc then report_call_chains post summary ;
let payload = postprocess post proc_desc in
Summary.update_summary payload summary
| None ->
summary