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.
418 lines
16 KiB
418 lines
16 KiB
(*
|
|
* Copyright (c) 2018-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 F = Format
|
|
module L = Logging
|
|
|
|
module Base = struct
|
|
type t = AccessPath.base
|
|
|
|
let compare = AccessPath.compare_base
|
|
|
|
let pp = AccessPath.pp_base
|
|
end
|
|
|
|
module VarSet = AbstractDomain.FiniteSet (Base)
|
|
|
|
module CapabilityDomain = struct
|
|
type astate =
|
|
| InvalidatedAt of Location.t
|
|
(** neither owned nor borrowed; we can't safely do anything with this value. tagged with the
|
|
location where invalidation occurred. *)
|
|
| BorrowedFrom of VarSet.astate
|
|
(** not owned, but borrowed from existing reference(s). for now, permits both reads and writes *)
|
|
| Owned
|
|
(** owned. permits reads, writes, and ownership transfer (e.g. call a destructor, delete, or free) *)
|
|
|
|
let make_borrowed_vars vars =
|
|
assert (not (VarSet.is_empty vars)) ;
|
|
BorrowedFrom vars
|
|
|
|
|
|
(** Owned <= BorrowedFrom <= InvalidatedAt *)
|
|
let ( <= ) ~lhs ~rhs =
|
|
if phys_equal lhs rhs then true
|
|
else
|
|
match (lhs, rhs) with
|
|
| InvalidatedAt loc1, InvalidatedAt loc2 ->
|
|
Location.compare loc1 loc2 <= 0
|
|
| _, InvalidatedAt _ ->
|
|
true
|
|
| InvalidatedAt _, _ ->
|
|
false
|
|
| BorrowedFrom s1, BorrowedFrom s2 ->
|
|
VarSet.( <= ) ~lhs:s1 ~rhs:s2
|
|
| Owned, _ ->
|
|
true
|
|
| _, Owned ->
|
|
false
|
|
|
|
|
|
let join astate1 astate2 =
|
|
if phys_equal astate1 astate2 then astate1
|
|
else
|
|
match (astate1, astate2) with
|
|
| BorrowedFrom s1, BorrowedFrom s2 ->
|
|
BorrowedFrom (VarSet.union s1 s2)
|
|
| Owned, astate | astate, Owned ->
|
|
astate
|
|
| InvalidatedAt loc1, InvalidatedAt loc2 ->
|
|
(* pick the "higher" program point that occurs syntactically later *)
|
|
if Location.compare loc1 loc2 >= 0 then astate1 else astate2
|
|
| (InvalidatedAt _ as invalid), _ | _, (InvalidatedAt _ as invalid) ->
|
|
invalid
|
|
|
|
|
|
let widen ~prev ~next ~num_iters:_ = join prev next
|
|
|
|
let pp fmt = function
|
|
| InvalidatedAt loc ->
|
|
F.fprintf fmt "InvalidatedAt(%a)" Location.pp loc
|
|
| BorrowedFrom vars ->
|
|
F.fprintf fmt "BorrowedFrom(%a)" VarSet.pp vars
|
|
| Owned ->
|
|
F.pp_print_string fmt "Owned"
|
|
end
|
|
|
|
let rec is_function_typ = function
|
|
| {Typ.desc= Tptr (t, _)} ->
|
|
is_function_typ t
|
|
| {Typ.desc= Tstruct typename} ->
|
|
String.is_prefix ~prefix:"std::function" (Typ.Name.name typename)
|
|
| _ ->
|
|
false
|
|
|
|
|
|
(** map from program variable to its capability *)
|
|
module Domain = struct
|
|
include AbstractDomain.Map (Base) (CapabilityDomain)
|
|
|
|
let report message loc ltr summary =
|
|
Reporting.log_error summary ~loc ~ltr IssueType.use_after_lifetime message
|
|
|
|
|
|
let report_return_stack_var (var, _) loc summary =
|
|
let message =
|
|
F.asprintf "Reference to stack variable %a is returned at %a" Var.pp var Location.pp loc
|
|
in
|
|
let ltr =
|
|
[ Errlog.make_trace_element 0 loc "Return of stack variable" []
|
|
; Errlog.make_trace_element 0 loc "End of procedure" [] ]
|
|
in
|
|
report message loc ltr summary
|
|
|
|
|
|
let report_use_after_lifetime (var, _) ~use_loc ~invalidated_loc summary =
|
|
if Var.appears_in_source_code var then
|
|
let message =
|
|
F.asprintf "Variable %a is used at line %a after its lifetime ended at %a" Var.pp var
|
|
Location.pp use_loc Location.pp invalidated_loc
|
|
in
|
|
let ltr =
|
|
[ Errlog.make_trace_element 0 invalidated_loc "End of variable lifetime" []
|
|
; Errlog.make_trace_element 0 use_loc "Use of invalid variable" [] ]
|
|
in
|
|
report message use_loc ltr summary
|
|
|
|
|
|
(* complain if we do not have the right capability to access [var] *)
|
|
let check_var_lifetime base_var use_loc summary astate =
|
|
let open CapabilityDomain in
|
|
match find base_var astate with
|
|
| InvalidatedAt invalidated_loc ->
|
|
report_use_after_lifetime base_var ~use_loc ~invalidated_loc summary
|
|
| BorrowedFrom borrowed_vars ->
|
|
(* warn if any of the borrowed vars are Invalid *)
|
|
let report_invalidated v =
|
|
match find v astate with
|
|
| InvalidatedAt invalidated_loc ->
|
|
report_use_after_lifetime base_var ~use_loc ~invalidated_loc summary
|
|
| BorrowedFrom _
|
|
(* TODO: can do deeper checking here, but have to worry about borrow cycles *)
|
|
| Owned ->
|
|
()
|
|
| exception Caml.Not_found ->
|
|
()
|
|
in
|
|
VarSet.iter report_invalidated borrowed_vars
|
|
| Owned ->
|
|
()
|
|
| exception Caml.Not_found ->
|
|
()
|
|
|
|
|
|
let base_add_read ((_, typ) as base_var) loc summary astate =
|
|
(* don't warn if it's a read of a std::function type. we model closures as borrowing their
|
|
captured vars, but simply reading a closure doesn't actually use these vars. this means that
|
|
we'll miss bugs involving invalid uses of std::function's, but that seems ok *)
|
|
if not (is_function_typ typ) then check_var_lifetime base_var loc summary astate ;
|
|
astate
|
|
|
|
|
|
let access_path_add_read (base, _) loc summary astate = base_add_read base loc summary astate
|
|
|
|
let exp_add_reads exp loc summary astate =
|
|
List.fold
|
|
~f:(fun acc access_expr ->
|
|
access_path_add_read (AccessExpression.to_access_path access_expr) loc summary acc )
|
|
(HilExp.get_access_exprs exp) ~init:astate
|
|
|
|
|
|
let actuals_add_reads actuals loc summary astate =
|
|
(* TODO: handle reads in actuals + return values properly. This is nontrivial because the
|
|
frontend sometimes chooses to translate return as pass-by-ref on a dummy actual *)
|
|
List.fold actuals
|
|
~f:(fun acc actual_exp -> exp_add_reads actual_exp loc summary acc)
|
|
~init:astate
|
|
|
|
|
|
let borrow_vars lhs_base rhs_vars astate =
|
|
(* borrow all of the vars read on the rhs *)
|
|
if VarSet.is_empty rhs_vars then remove lhs_base astate
|
|
else add lhs_base (CapabilityDomain.make_borrowed_vars rhs_vars) astate
|
|
|
|
|
|
(* copy capability if it exists, consider borrowing if it doesn't *)
|
|
let copy_or_borrow_var lhs_base rhs_base astate =
|
|
try
|
|
let rhs_capability = find rhs_base astate in
|
|
add lhs_base rhs_capability astate
|
|
with Caml.Not_found ->
|
|
if Var.is_cpp_temporary (fst rhs_base) then
|
|
borrow_vars lhs_base (VarSet.singleton rhs_base) astate
|
|
else add lhs_base CapabilityDomain.Owned astate
|
|
|
|
|
|
(* handle assigning directly to a base var *)
|
|
let handle_var_assign ?(is_operator_equal = false) lhs_base rhs_exp loc summary astate =
|
|
match (rhs_exp : HilExp.t) with
|
|
| Constant _ when not (Var.is_cpp_temporary (fst lhs_base)) ->
|
|
add lhs_base CapabilityDomain.Owned astate
|
|
| AccessExpression (Base rhs_base | AddressOf (Base rhs_base))
|
|
when not (Var.appears_in_source_code (fst rhs_base)) ->
|
|
copy_or_borrow_var lhs_base rhs_base astate
|
|
| Closure (_, captured_vars) ->
|
|
(* TODO: can be folded into the case above once we have proper AccessExpressions *)
|
|
let vars_captured_by_ref =
|
|
List.fold captured_vars
|
|
~f:(fun acc (((_, typ) as base), _) ->
|
|
match typ.Typ.desc with Typ.Tptr _ -> VarSet.add base acc | _ -> acc )
|
|
~init:VarSet.empty
|
|
in
|
|
borrow_vars lhs_base vars_captured_by_ref astate
|
|
| AccessExpression (Base rhs_base)
|
|
when (not is_operator_equal) && Typ.is_reference (snd rhs_base) ->
|
|
copy_or_borrow_var lhs_base rhs_base astate
|
|
| AccessExpression (AddressOf (Base rhs_base)) when not is_operator_equal ->
|
|
borrow_vars lhs_base (VarSet.singleton rhs_base) astate
|
|
| _ ->
|
|
(* TODO: support capability transfer between source vars, other assignment modes *)
|
|
exp_add_reads rhs_exp loc summary astate |> remove lhs_base
|
|
|
|
|
|
let release_ownership base loc summary astate =
|
|
base_add_read base loc summary astate |> add base (CapabilityDomain.InvalidatedAt loc)
|
|
end
|
|
|
|
module TransferFunctions (CFG : ProcCfg.S) = struct
|
|
module CFG = CFG
|
|
module Domain = Domain
|
|
|
|
type extras = Summary.t
|
|
|
|
let is_aggregate (_, typ) =
|
|
match typ.Typ.desc with
|
|
| Tstruct _ ->
|
|
(* TODO: we could compute this precisely by recursively checking all of the fields of the
|
|
type, but that's going to be expensive. will add it to the frontend instead *)
|
|
true
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let get_assigned_base (access_expression : AccessExpression.t) =
|
|
match access_expression with
|
|
| Base base ->
|
|
Some base
|
|
| _ ->
|
|
let base = AccessExpression.get_base access_expression in
|
|
(* assume assigning to any field of an aggregate struct re-initalizes the struct *)
|
|
Option.some_if (is_aggregate base) base
|
|
|
|
|
|
let returns_struct pname =
|
|
(* Assumption: we add an extra return param for structs *)
|
|
Ondemand.get_proc_desc pname
|
|
|> Option.value_map ~default:false ~f:Procdesc.has_added_return_param
|
|
|
|
|
|
let acquire_ownership call_exp return_base actuals loc summary astate =
|
|
let aquire_ownership_of_first_param actuals =
|
|
match actuals with
|
|
| HilExp.AccessExpression (AccessExpression.AddressOf access_expression) :: other_actuals -> (
|
|
match get_assigned_base access_expression with
|
|
| Some constructed_base ->
|
|
let astate' = Domain.actuals_add_reads other_actuals loc summary astate in
|
|
Some (Domain.add constructed_base CapabilityDomain.Owned astate')
|
|
| None ->
|
|
Some astate )
|
|
| _ ->
|
|
Some astate
|
|
in
|
|
match call_exp with
|
|
| HilInstr.Direct pname ->
|
|
(* TODO: support new[], malloc, others? *)
|
|
if Typ.Procname.equal pname BuiltinDecl.__new then
|
|
let astate' = Domain.actuals_add_reads actuals loc summary astate in
|
|
Some (Domain.add return_base CapabilityDomain.Owned astate')
|
|
else if Typ.Procname.equal pname BuiltinDecl.__placement_new then
|
|
match List.rev actuals with
|
|
| HilExp.AccessExpression (Base placement_base) :: other_actuals ->
|
|
(* TODO: placement new creates an alias between return var and placement var, should
|
|
eventually model as return borrowing from placement (see
|
|
FN_placement_new_aliasing2_bad test) *)
|
|
Domain.actuals_add_reads other_actuals loc summary astate
|
|
|> Domain.add placement_base CapabilityDomain.Owned
|
|
|> Domain.add return_base CapabilityDomain.Owned
|
|
|> Option.some
|
|
| _ :: other_actuals ->
|
|
Domain.actuals_add_reads other_actuals loc summary astate
|
|
|> Domain.add return_base CapabilityDomain.Owned
|
|
|> Option.some
|
|
| _ ->
|
|
L.die InternalError "Placement new without placement in %a %a" Typ.Procname.pp pname
|
|
Location.pp loc
|
|
else if Typ.Procname.is_constructor pname then aquire_ownership_of_first_param actuals
|
|
else if returns_struct pname then aquire_ownership_of_first_param (List.rev actuals)
|
|
else None
|
|
| HilInstr.Indirect _ ->
|
|
None
|
|
|
|
|
|
let is_destructor = function
|
|
| Typ.Procname.ObjC_Cpp clang_pname ->
|
|
Typ.Procname.ObjC_Cpp.is_destructor clang_pname
|
|
&& not
|
|
(* Our frontend generates synthetic inner destructors to model invocation of base class
|
|
destructors correctly; see D5834555/D7189239 *)
|
|
(Typ.Procname.ObjC_Cpp.is_inner_destructor clang_pname)
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let is_early_return call_exp =
|
|
match call_exp with
|
|
| HilInstr.Direct pname ->
|
|
Typ.Procname.equal pname BuiltinDecl.exit
|
|
|| Typ.Procname.equal pname BuiltinDecl.objc_cpp_throw
|
|
|| Typ.Procname.equal pname BuiltinDecl.abort
|
|
| _ ->
|
|
false
|
|
|
|
|
|
let exec_instr (astate : Domain.astate) (proc_data : extras ProcData.t) _ (instr : HilInstr.t) =
|
|
let summary = proc_data.extras in
|
|
match instr with
|
|
| Assign (lhs_access_exp, rhs_exp, loc) -> (
|
|
match get_assigned_base lhs_access_exp with
|
|
| Some lhs_base ->
|
|
Domain.handle_var_assign lhs_base rhs_exp loc summary astate
|
|
| None ->
|
|
(* assign to field, array, indirectly with &/*, or a combination *)
|
|
Domain.exp_add_reads rhs_exp loc summary astate
|
|
|> Domain.access_path_add_read
|
|
(AccessExpression.to_access_path lhs_access_exp)
|
|
loc summary )
|
|
| Call (_, Direct callee_pname, _, _, _)
|
|
when Typ.Procname.equal callee_pname BuiltinDecl.__variable_initialization ->
|
|
astate
|
|
| Call (_, Direct callee_pname, [AccessExpression (Base lhs_base)], _, loc)
|
|
when Typ.Procname.equal callee_pname BuiltinDecl.__delete ->
|
|
(* TODO: support delete[], free, and (in some cases) std::move *)
|
|
Domain.release_ownership lhs_base loc summary astate
|
|
| Call (_, Direct callee_pname, [AccessExpression (AddressOf (Base lhs_base))], _, loc)
|
|
when is_destructor callee_pname ->
|
|
Domain.release_ownership lhs_base loc summary astate
|
|
| Call
|
|
( _
|
|
, Direct (Typ.Procname.ObjC_Cpp callee_pname)
|
|
, [AccessExpression (AddressOf (Base lhs_base)); rhs_exp]
|
|
, _
|
|
, loc )
|
|
when Typ.Procname.ObjC_Cpp.is_operator_equal callee_pname ->
|
|
(* TODO: once we go interprocedural, this case should only apply for operator='s with an
|
|
empty summary *)
|
|
Domain.handle_var_assign ~is_operator_equal:true lhs_base rhs_exp loc summary astate
|
|
| Call
|
|
( _
|
|
, Direct (Typ.Procname.ObjC_Cpp callee_pname)
|
|
, AccessExpression (AddressOf (Base lhs_base)) :: _
|
|
, _
|
|
, loc )
|
|
when Typ.Procname.ObjC_Cpp.is_cpp_lambda callee_pname ->
|
|
(* invoking a lambda; check that its captured vars are valid *)
|
|
Domain.check_var_lifetime lhs_base loc summary astate ;
|
|
astate
|
|
| Call (ret_id_typ, call_exp, actuals, _, loc) -> (
|
|
match acquire_ownership call_exp ret_id_typ actuals loc summary astate with
|
|
| Some astate' ->
|
|
astate'
|
|
| None ->
|
|
if is_early_return call_exp then
|
|
(* thrown exception, abort(), or exit; return _|_ *)
|
|
Domain.empty
|
|
else
|
|
let astate' = Domain.actuals_add_reads actuals loc summary astate in
|
|
Domain.remove ret_id_typ astate' )
|
|
| Assume (assume_exp, _, _, loc) ->
|
|
Domain.exp_add_reads assume_exp loc summary astate
|
|
|
|
|
|
let pp_session_name _node fmt = F.pp_print_string fmt "ownership"
|
|
end
|
|
|
|
module Analyzer = LowerHil.MakeAbstractInterpreter (ProcCfg.Exceptional) (TransferFunctions)
|
|
|
|
let report_invalid_return post end_loc summary =
|
|
let locals =
|
|
Procdesc.get_locals summary.Summary.proc_desc |> List.map ~f:(fun {ProcAttributes.name} -> name)
|
|
in
|
|
let is_local_to_procedure var =
|
|
Var.get_mangled var
|
|
|> Option.value_map ~default:false ~f:(fun mangled ->
|
|
List.mem ~equal:Mangled.equal locals mangled )
|
|
in
|
|
(* look for return values that are borrowed from (now-invalid) local variables *)
|
|
let report_invalid_return base (capability : CapabilityDomain.astate) =
|
|
if Var.is_return (fst base) then
|
|
match capability with
|
|
| BorrowedFrom vars ->
|
|
VarSet.iter
|
|
(fun ((var, _) as borrowed_base) ->
|
|
if is_local_to_procedure var then
|
|
Domain.report_return_stack_var borrowed_base end_loc summary )
|
|
vars
|
|
| InvalidatedAt invalidated_loc ->
|
|
Domain.report_use_after_lifetime base ~use_loc:end_loc ~invalidated_loc summary
|
|
| Owned ->
|
|
()
|
|
in
|
|
Domain.iter report_invalid_return post
|
|
|
|
|
|
let checker {Callbacks.proc_desc; tenv; summary} =
|
|
let proc_data = ProcData.make proc_desc tenv summary in
|
|
let initial = Domain.empty in
|
|
( match Analyzer.compute_post proc_data ~initial with
|
|
| Some post ->
|
|
let end_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_desc) in
|
|
report_invalid_return post end_loc summary
|
|
| None ->
|
|
() ) ;
|
|
summary
|