(* * 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 Access = struct type t = | Read of AccessPath.t | Write of AccessPath.t | ContainerRead of AccessPath.t * Typ.Procname.t | ContainerWrite of AccessPath.t * Typ.Procname.t | InterfaceCall of Typ.Procname.t [@@deriving compare] let suffix_matches (_, accesses1) (_, accesses2) = match (List.rev accesses1, List.rev accesses2) with | access1 :: _, access2 :: _ -> AccessPath.equal_access access1 access2 | _ -> false let matches ~caller ~callee = match (caller, callee) with | Read ap1, Read ap2 | Write ap1, Write ap2 -> suffix_matches ap1 ap2 | ContainerRead (ap1, pname1), ContainerRead (ap2, pname2) | ContainerWrite (ap1, pname1), ContainerWrite (ap2, pname2) -> Typ.Procname.equal pname1 pname2 && suffix_matches ap1 ap2 | InterfaceCall pname1, InterfaceCall pname2 -> Typ.Procname.equal pname1 pname2 | _ -> false let make_field_access access_path ~is_write = if is_write then Write access_path else Read access_path let get_access_path = function | Read access_path | Write access_path | ContainerWrite (access_path, _) | ContainerRead (access_path, _) -> Some access_path | InterfaceCall _ -> None let map ~f = function | Read access_path -> Read (f access_path) | Write access_path -> Write (f access_path) | ContainerWrite (access_path, pname) -> ContainerWrite (f access_path, pname) | ContainerRead (access_path, pname) -> ContainerRead (f access_path, pname) | InterfaceCall _ as intfcall -> intfcall let equal t1 t2 = Int.equal (compare t1 t2) 0 let pp fmt = function | Read access_path -> F.fprintf fmt "Read of %a" AccessPath.pp access_path | Write access_path -> F.fprintf fmt "Write to %a" AccessPath.pp access_path | ContainerRead (access_path, pname) -> F.fprintf fmt "Read of container %a via %a" AccessPath.pp access_path Typ.Procname.pp pname | ContainerWrite (access_path, pname) -> F.fprintf fmt "Write to container %a via %a" AccessPath.pp access_path Typ.Procname.pp pname | InterfaceCall pname -> F.fprintf fmt "Call to un-annotated interface method %a" Typ.Procname.pp pname end module TraceElem = struct module Kind = Access type t = {site: CallSite.t; kind: Kind.t} [@@deriving compare] let is_write {kind} = match kind with | InterfaceCall _ | Read _ | ContainerRead _ -> false | ContainerWrite _ | Write _ -> true let is_container_write {kind} = match kind with | InterfaceCall _ | Read _ | Write _ | ContainerRead _ -> false | ContainerWrite _ -> true let call_site {site} = site let kind {kind} = kind let make ?indexes:_ kind site = {kind; site} let with_callsite t site = {t with site} let pp fmt {site; kind} = F.fprintf fmt "%a at %a" Access.pp kind CallSite.pp site let map ~f {site; kind} = {site; kind= Access.map ~f kind} module Set = PrettyPrintable.MakePPSet (struct type nonrec t = t let compare = compare let pp = pp end) let dummy_pname = Typ.Procname.empty_block let make_dummy_site = CallSite.make dummy_pname (* all trace elems are created with a dummy call site. any trace elem without a dummy call site represents a call that leads to an access rather than a direct access *) let is_direct {site} = Typ.Procname.equal (CallSite.pname site) dummy_pname let make_container_access access_path pname ~is_write loc = let site = make_dummy_site loc in let access = if is_write then Access.ContainerWrite (access_path, pname) else Access.ContainerRead (access_path, pname) in make access site let make_field_access access_path ~is_write loc = let site = make_dummy_site loc in make (Access.make_field_access access_path ~is_write) site let make_unannotated_call_access pname loc = let site = make_dummy_site loc in make (Access.InterfaceCall pname) site end module LockCount = AbstractDomain.CountDomain (struct let max = 5 (* arbitrary threshold for max locks we expect to be held simultaneously *) end) module LocksDomain = struct include AbstractDomain.Map (AccessPath) (LockCount) (* TODO: eventually, we'll ask add_lock/remove_lock to pass the lock name. for now, this is a hack to model having a single lock used everywhere *) let the_only_lock = ((Var.of_id (Ident.create Ident.knormal 0), Typ.void_star), []) let is_locked astate = (* we only add locks with positive count, so safe to just check emptiness *) not (is_empty astate) let lookup_count lock astate = try find lock astate with Not_found -> LockCount.empty let add_lock astate = let count = lookup_count the_only_lock astate in add the_only_lock (LockCount.increment count) astate let remove_lock astate = let count = lookup_count the_only_lock astate in try let count' = LockCount.decrement count in if LockCount.is_empty count' then remove the_only_lock astate else add the_only_lock count' astate with Not_found -> astate let integrate_summary ~caller_astate ~callee_astate = let caller_count = lookup_count the_only_lock caller_astate in let callee_count = lookup_count the_only_lock callee_astate in let sum = LockCount.add caller_count callee_count in if LockCount.is_empty sum then caller_astate else add the_only_lock sum caller_astate end module ThreadsDomain = struct type astate = NoThread | AnyThreadButSelf | AnyThread [@@deriving compare] let empty = NoThread (* NoThread < AnyThreadButSelf < Any *) let ( <= ) ~lhs ~rhs = match (lhs, rhs) with | NoThread, _ -> true | _, NoThread -> false | _, AnyThread -> true | AnyThread, _ -> false | _ -> Int.equal 0 (compare_astate lhs rhs) let join astate1 astate2 = match (astate1, astate2) with | NoThread, astate | astate, NoThread -> astate | AnyThread, _ | _, AnyThread -> AnyThread | AnyThreadButSelf, AnyThreadButSelf -> AnyThreadButSelf let widen ~prev ~next ~num_iters:_ = join prev next let pp fmt astate = F.fprintf fmt ( match astate with | NoThread -> "NoThread" | AnyThreadButSelf -> "AnyThreadButSelf" | AnyThread -> "AnyThread" ) let is_empty = function NoThread -> true | _ -> false let is_any_but_self = function AnyThreadButSelf -> true | _ -> false let is_any = function AnyThread -> true | _ -> false end module PathDomain = SinkTrace.Make (TraceElem) module Choice = struct type t = OnMainThread | LockHeld [@@deriving compare] let pp fmt = function | OnMainThread -> F.fprintf fmt "OnMainThread" | LockHeld -> F.fprintf fmt "LockHeld" end module Attribute = struct type t = Functional | Choice of Choice.t [@@deriving compare] let pp fmt = function | Functional -> F.fprintf fmt "Functional" | Choice choice -> Choice.pp fmt choice module Set = PrettyPrintable.MakePPSet (struct type nonrec t = t let compare = compare let pp = pp end) end module AttributeSetDomain = AbstractDomain.InvertedSet (Attribute) module OwnershipAbstractValue = struct type astate = Owned | OwnedIf of IntSet.t | Unowned [@@deriving compare] let owned = Owned let unowned = Unowned let make_owned_if formal_index = OwnedIf (IntSet.singleton formal_index) let ( <= ) ~lhs ~rhs = if phys_equal lhs rhs then true else match (lhs, rhs) with | _, Unowned -> true (* Unowned is top *) | Unowned, _ -> false | Owned, _ -> true (* Owned is bottom *) | OwnedIf s1, OwnedIf s2 -> IntSet.subset s1 s2 | OwnedIf _, Owned -> false let join astate1 astate2 = if phys_equal astate1 astate2 then astate1 else match (astate1, astate2) with | _, Unowned | Unowned, _ -> Unowned | astate, Owned | Owned, astate -> astate | OwnedIf s1, OwnedIf s2 -> OwnedIf (IntSet.union s1 s2) let widen ~prev ~next ~num_iters:_ = join prev next let pp fmt = function | Unowned -> F.fprintf fmt "Unowned" | OwnedIf s -> F.fprintf fmt "OwnedIf%a" (PrettyPrintable.pp_collection ~pp_item:Int.pp) (IntSet.elements s) | Owned -> F.fprintf fmt "Owned" end module OwnershipDomain = struct include AbstractDomain.Map (AccessPath) (OwnershipAbstractValue) (* Helper function used by both is_owned and get_owned. Not exported.*) let get_owned_shallow access_path astate = try find access_path astate with Not_found -> OwnershipAbstractValue.Unowned (*deep ownership model where only a prefix needs to be owned in the astate*) let is_owned (base, accesses) astate = let is_owned_shallow access_path astate = match get_owned_shallow access_path astate with | OwnershipAbstractValue.Owned -> true | _ -> false in let rec helper = function | prefix, _ when is_owned_shallow (base, prefix) astate -> true | _, [] -> false | prefix, hd :: tl -> helper (List.append prefix [hd], tl) in helper ([], accesses) (* returns Owned if any prefix is owned on any prefix, else OwnedIf if it is OwnedIf in the astate, else UnOwned *) let get_owned access_path astate = if is_owned access_path astate then OwnershipAbstractValue.Owned else try find access_path astate with Not_found -> OwnershipAbstractValue.Unowned let find = `Use_get_owned_instead end module AttributeMapDomain = struct include AbstractDomain.InvertedMap (AccessPath) (AttributeSetDomain) let add access_path attribute_set t = if AttributeSetDomain.is_empty attribute_set then t else add access_path attribute_set t let has_attribute access_path attribute t = try find access_path t |> AttributeSetDomain.mem attribute with Not_found -> false let get_choices access_path t = try let attributes = find access_path t in List.filter_map ~f:(function Attribute.Choice c -> Some c | _ -> None) (AttributeSetDomain.elements attributes) with Not_found -> [] let add_attribute access_path attribute t = let attribute_set = (try find access_path t with Not_found -> AttributeSetDomain.empty) |> AttributeSetDomain.add attribute in add access_path attribute_set t end module AccessData = struct module Precondition = struct type t = Conjunction of IntSet.t | False [@@deriving compare] (* precondition is true if the conjunction of owned indexes is empty *) let is_true = function False -> false | Conjunction set -> IntSet.is_empty set let pp fmt = function | Conjunction indexes -> F.fprintf fmt "Owned(%a)" (PrettyPrintable.pp_collection ~pp_item:Int.pp) (IntSet.elements indexes) | False -> F.fprintf fmt "False" end type t = {thread: bool; lock: bool; ownership_precondition: Precondition.t} [@@deriving compare] let make lock threads ownership_precondition pdesc = (* shouldn't be creating metadata for accesses that are known to be owned; we should discard such accesses *) assert (not (Precondition.is_true ownership_precondition)) ; let thread = ThreadsDomain.is_any_but_self threads in let lock = LocksDomain.is_locked lock || Procdesc.is_java_synchronized pdesc in {thread; lock; ownership_precondition} let is_unprotected {thread; lock; ownership_precondition} = not thread && not lock && not (Precondition.is_true ownership_precondition) let pp fmt {thread; lock; ownership_precondition} = F.fprintf fmt "Thread: %b Lock: %b Pre: %a" thread lock Precondition.pp ownership_precondition end module AccessDomain = struct include AbstractDomain.Map (AccessData) (PathDomain) let add_access precondition access_path t = let precondition_accesses = try find precondition t with Not_found -> PathDomain.empty in let precondition_accesses' = PathDomain.add_sink access_path precondition_accesses in add precondition precondition_accesses' t let get_accesses precondition t = try find precondition t with Not_found -> PathDomain.empty end module StabilityDomain = struct include AccessTree.PathSet (AccessTree.DefaultConfig) let get_all_paths t = fold (fun paths z _ -> match z with AccessPath.Abs.Abstracted p | AccessPath.Abs.Exact p -> p :: paths ) t [] let add_path (access_path: AccessPath.t) t = let (base, _), accesses = access_path in (* Without this check, short prefixes will override longer paths in the tree *) if mem (AccessPath.Abs.Abstracted access_path) t then t else match (base, accesses) with (* Do not add a vanilla "this" as a prefix *) | Var.ProgramVar pvar, [] when Pvar.is_this pvar -> t | _ -> let ap_abs = AccessPath.Abs.Exact access_path in add_trace ap_abs true t let add_wobbly_path_exp exp paths = let access_paths = HilExp.get_access_paths exp in List.fold ~f:(fun acc p -> add_path p acc) access_paths ~init:paths let add_wobbly_actuals exps paths = (* Checking conditions for inter-procedural stability: automatically add duplicating actuals, "deep" stability checking will be handled by processing the method's footprint and rebasing (see StabilityDomain.rebase_paths). *) let is_dup_access_path e = List.count exps ~f:(fun x -> match (x, e) with | HilExp.AccessExpression ae1, HilExp.AccessExpression ae2 -> AccessPath.equal (AccessExpression.to_access_path ae1) (AccessExpression.to_access_path ae2) | _, _ -> false ) > 1 in let is_prefix_exp x e = match (x, e) with | HilExp.AccessExpression ae1, HilExp.AccessExpression ae2 -> let open AccessExpression in let ap1, ap2 = (to_access_path ae1, to_access_path ae2) in AccessPath.is_prefix ap1 ap2 && not (AccessPath.equal ap1 ap2) | _ -> false in let find_all_prefixes e = List.filter exps ~f:(fun x -> is_prefix_exp x e) in let with_duplicates = List.fold exps ~init:paths ~f:(fun acc e -> if is_dup_access_path e then add_wobbly_path_exp e acc else acc ) in let with_prefixes = List.fold exps ~init:with_duplicates ~f:(fun acc_paths e -> List.fold (find_all_prefixes e) ~init:acc_paths ~f:(fun acc1 e1 -> add_wobbly_path_exp e1 acc1 ) ) in with_prefixes let add_wobbly_paths_assign lhs_path rhs_exp paths = let paths_with_lhs = add_path lhs_path paths in let paths_with_lsh_rhs = add_wobbly_path_exp rhs_exp paths_with_lhs in paths_with_lsh_rhs let rebase_paths actuals pdesc t = let formal_map = FormalMap.make pdesc in let remove_pure_formals paths = List.filter paths ~f:(fun p -> match p with | base, [] when FormalMap.get_formal_index base formal_map |> Option.is_some -> false | _ -> true ) in let expand_path ((base, accesses) as path) = match FormalMap.get_formal_index base formal_map with | Some formal_index -> ( match List.nth actuals formal_index with | Some actual_exp -> ( match HilExp.get_access_paths actual_exp with | [actual] -> AccessPath.append actual accesses | _ -> path ) | None -> path ) | None -> path in get_all_paths t |> remove_pure_formals |> List.map ~f:expand_path |> List.fold ~f:(fun acc p -> add_path p acc) ~init:empty end type astate = { threads: ThreadsDomain.astate ; locks: LocksDomain.astate ; accesses: AccessDomain.astate ; ownership: OwnershipDomain.astate ; attribute_map: AttributeMapDomain.astate ; wobbly_paths: StabilityDomain.astate } let empty = let threads = ThreadsDomain.empty in let locks = LocksDomain.empty in let accesses = AccessDomain.empty in let ownership = OwnershipDomain.empty in let attribute_map = AttributeMapDomain.empty in let wobbly_paths = StabilityDomain.empty in {threads; locks; accesses; ownership; attribute_map; wobbly_paths} let is_empty {threads; locks; accesses; ownership; attribute_map; wobbly_paths} = ThreadsDomain.is_empty threads && LocksDomain.is_empty locks && AccessDomain.is_empty accesses && OwnershipDomain.is_empty ownership && AttributeMapDomain.is_empty attribute_map && StabilityDomain.is_empty wobbly_paths let ( <= ) ~lhs ~rhs = if phys_equal lhs rhs then true else ThreadsDomain.( <= ) ~lhs:lhs.threads ~rhs:rhs.threads && LocksDomain.( <= ) ~lhs:lhs.locks ~rhs:rhs.locks && AccessDomain.( <= ) ~lhs:lhs.accesses ~rhs:rhs.accesses && AttributeMapDomain.( <= ) ~lhs:lhs.attribute_map ~rhs:rhs.attribute_map && StabilityDomain.( <= ) ~lhs:lhs.wobbly_paths ~rhs:rhs.wobbly_paths let join astate1 astate2 = if phys_equal astate1 astate2 then astate1 else let threads = ThreadsDomain.join astate1.threads astate2.threads in let locks = LocksDomain.join astate1.locks astate2.locks in let accesses = AccessDomain.join astate1.accesses astate2.accesses in let ownership = OwnershipDomain.join astate1.ownership astate2.ownership in let attribute_map = AttributeMapDomain.join astate1.attribute_map astate2.attribute_map in let wobbly_paths = StabilityDomain.join astate1.wobbly_paths astate2.wobbly_paths in {threads; locks; accesses; ownership; attribute_map; wobbly_paths} let widen ~prev ~next ~num_iters = if phys_equal prev next then prev else let threads = ThreadsDomain.widen ~prev:prev.threads ~next:next.threads ~num_iters in let locks = LocksDomain.widen ~prev:prev.locks ~next:next.locks ~num_iters in let accesses = AccessDomain.widen ~prev:prev.accesses ~next:next.accesses ~num_iters in let ownership = OwnershipDomain.widen ~prev:prev.ownership ~next:next.ownership ~num_iters in let attribute_map = AttributeMapDomain.widen ~prev:prev.attribute_map ~next:next.attribute_map ~num_iters in let wobbly_paths = StabilityDomain.widen ~prev:prev.wobbly_paths ~next:next.wobbly_paths ~num_iters in {threads; locks; accesses; ownership; attribute_map; wobbly_paths} type summary = { threads: ThreadsDomain.astate ; locks: LocksDomain.astate ; accesses: AccessDomain.astate ; return_ownership: OwnershipAbstractValue.astate ; return_attributes: AttributeSetDomain.astate ; wobbly_paths: StabilityDomain.astate } let pp_summary fmt {threads; locks; accesses; return_ownership; return_attributes; wobbly_paths} = F.fprintf fmt "@\n\ Threads: %a, Locks: %a @\n\ Accesses %a @\n\ Ownership: %a @\n\ Return Attributes: %a @\n\ Wobbly Paths: %a@\n\ " ThreadsDomain.pp threads LocksDomain.pp locks AccessDomain.pp accesses OwnershipAbstractValue.pp return_ownership AttributeSetDomain.pp return_attributes StabilityDomain.pp wobbly_paths let pp fmt {threads; locks; accesses; ownership; attribute_map; wobbly_paths} = F.fprintf fmt "Threads: %a, Locks: %a @\n\ Accesses %a @\n \ Ownership: %a @\n\ Attributes: %a @\n\ Non-stable Paths: %a@\n\ " ThreadsDomain.pp threads LocksDomain.pp locks AccessDomain.pp accesses OwnershipDomain.pp ownership AttributeMapDomain.pp attribute_map StabilityDomain.pp wobbly_paths let rec attributes_of_expr attribute_map e = let open HilExp in match e with | HilExp.AccessExpression access_expr -> ( try AttributeMapDomain.find (AccessExpression.to_access_path access_expr) attribute_map with Not_found -> AttributeSetDomain.empty ) | Constant _ -> AttributeSetDomain.of_list [Attribute.Functional] | Exception expr (* treat exceptions as transparent wrt attributes *) | Cast (_, expr) -> attributes_of_expr attribute_map expr | UnaryOperator (_, expr, _) -> attributes_of_expr attribute_map expr | BinaryOperator (_, expr1, expr2) -> let attributes1 = attributes_of_expr attribute_map expr1 in let attributes2 = attributes_of_expr attribute_map expr2 in AttributeSetDomain.join attributes1 attributes2 | Closure _ | Sizeof _ -> AttributeSetDomain.empty let rec ownership_of_expr expr ownership = let open HilExp in match expr with | AccessExpression access_expr -> OwnershipDomain.get_owned (AccessExpression.to_access_path access_expr) ownership | Constant _ -> OwnershipAbstractValue.owned | Exception e (* treat exceptions as transparent wrt ownership *) | Cast (_, e) -> ownership_of_expr e ownership | _ -> OwnershipAbstractValue.unowned let filter_by_access access_filter trace = PathDomain.Sinks.filter access_filter (PathDomain.sinks trace) |> PathDomain.update_sinks trace let get_all_accesses_with_pre pre_filter access_filter accesses = AccessDomain.fold (fun pre trace acc -> if pre_filter pre then PathDomain.join (filter_by_access access_filter trace) acc else acc ) accesses PathDomain.empty let get_all_accesses = get_all_accesses_with_pre (fun _ -> true)