resource leaks tutorial

Summary: Publish solutions to the lab, and a Docker file and image to get started more quickly with infer hacking.

Reviewed By: mbouaziz

Differential Revision: D13648847

fbshipit-source-id: daf48ad03
master
Jules Villard 6 years ago committed by Facebook Github Bot
parent 9e91c9298b
commit 16c0c03050

@ -117,7 +117,6 @@ DIRECT_TESTS += \
java_hoisting \ java_hoisting \
java_hoistingExpensive \ java_hoistingExpensive \
java_infer \ java_infer \
java_lab \
java_performance \ java_performance \
java_purity \ java_purity \
java_quandary \ java_quandary \

@ -0,0 +1,65 @@
FROM debian:stretch-slim
LABEL maintainer "Infer team"
# mkdir the man/man1 directory due to Debian bug #863199
RUN apt-get update && \
mkdir -p /usr/share/man/man1 && \
apt-get install --yes --no-install-recommends \
autoconf \
automake \
bzip2 \
cmake \
curl \
gcc \
git \
libc6-dev \
libgmp-dev \
libmpfr-dev \
libsqlite3-dev \
make \
openjdk-8-jdk-headless \
patch \
pkg-config \
python2.7 \
unzip \
zlib1g-dev && \
rm -rf /var/lib/apt/lists/*
# Some scripts in facebook-clang-plugins assume "python" is available
RUN cd /usr/local/bin && ln -s /usr/bin/python2.7 python
# Install opam 2
RUN curl -sL https://github.com/ocaml/opam/releases/download/2.0.2/opam-2.0.2-x86_64-linux > /usr/bin/opam && \
chmod +x /usr/bin/opam
# Disable sandboxing
# Without this opam fails to compile OCaml for some reason. We don't need sandboxing inside a Docker container anyway.
RUN opam init --reinit --bare --disable-sandboxing
# Download the latest Infer master
RUN cd / && \
git clone https://github.com/facebook/infer/
# Build opam deps first, then infer. This way if any step fails we
# don't lose the significant amount of work done in the previous
# steps.
RUN cd /infer && \
INFER_OPAM_SWITCH=4.07.1 ./build-infer.sh --only-setup-opam --no-opam-lock java && \
opam clean
# Make sure clang is disabled
RUN eval $(opam env) && \
cd /infer && \
SKIP_SUBMODULES=true ./autogen.sh && \
./configure --disable-c-analyzers
# Install Infer
ENV INFER_HOME /infer/infer
ENV PATH ${INFER_HOME}/bin:${PATH}
# build in non-optimized mode by default to speed up build times
ENV BUILD_MODE=default
# prevent exiting by compulsively hitting Control-D
ENV IGNOREEOF=9

@ -0,0 +1,73 @@
FROM debian:stretch-slim
LABEL maintainer "Infer team"
# mkdir the man/man1 directory due to Debian bug #863199
RUN apt-get update && \
mkdir -p /usr/share/man/man1 && \
apt-get install --yes --no-install-recommends \
autoconf \
automake \
bubblewrap \
bzip2 \
cmake \
curl \
g++ \
gcc \
git \
libc6-dev \
libgmp-dev \
libmpfr-dev \
libsqlite3-dev \
make \
openjdk-8-jdk-headless \
patch \
pkg-config \
python2.7 \
unzip \
xz-utils \
zlib1g-dev && \
rm -rf /var/lib/apt/lists/*
# Some scripts in facebook-clang-plugins assume "python" is available
RUN cd /usr/local/bin && ln -s /usr/bin/python2.7 python
# Install opam 2
RUN curl -sL https://github.com/ocaml/opam/releases/download/2.0.2/opam-2.0.2-x86_64-linux > /usr/bin/opam && \
chmod +x /usr/bin/opam
# Disable sandboxing
# Without this opam fails to compile OCaml for some reason. We don't need sandboxing inside a Docker container anyway.
RUN opam init --reinit --bare --disable-sandboxing
# Download the latest Infer master
RUN cd / && \
git clone --recurse-submodules https://github.com/facebook/infer/
# Build opam deps first, then clang, then infer. This way if any step
# fails we don't lose the significant amount of work done in the
# previous steps.
RUN cd /infer && ./build-infer.sh --only-setup-opam
RUN cd /infer && \
eval $(opam env) && \
./autogen.sh && \
./configure && \
./facebook-clang-plugins/clang/setup.sh
# Hackish for now: pull to get the latest version
RUN cd /infer && git pull
# if called with /infer-host mounted then copy infer there
RUN if test -d /infer-host; then \
cp -av /infer/. /infer-host; \
fi
# Install Infer
ENV INFER_HOME /infer/infer
ENV PATH ${INFER_HOME}/bin:${PATH}
# build in non-optimized mode by default to speed up build times
ENV BUILD_MODE=default
# prevent exiting by compulsively hitting Control-D
ENV IGNOREEOF=9

@ -190,20 +190,34 @@ module Raw = struct
let append (base, old_accesses) new_accesses = (base, old_accesses @ new_accesses) let append (base, old_accesses) new_accesses = (base, old_accesses @ new_accesses)
let rec is_prefix_path path1 path2 = let rec chop_prefix_path ~prefix:path1 path2 =
if phys_equal path1 path2 then true if phys_equal path1 path2 then Some []
else else
match (path1, path2) with match (path1, path2) with
| [], _ -> | [], remaining ->
true Some remaining
| _, [] -> | _, [] ->
false None
| access1 :: p1, access2 :: p2 -> | access1 :: prefix, access2 :: rest when equal_access access1 access2 ->
equal_access access1 access2 && is_prefix_path p1 p2 chop_prefix_path ~prefix rest
| _ ->
None
let chop_prefix ~prefix:((base1, path1) as ap1) ((base2, path2) as ap2) =
if phys_equal ap1 ap2 then Some []
else if equal_base base1 base2 then chop_prefix_path ~prefix:path1 path2
else None
let is_prefix ((base1, path1) as ap1) ((base2, path2) as ap2) =
if phys_equal ap1 ap2 then true else equal_base base1 base2 && is_prefix_path path1 path2 let is_prefix ap1 ap2 = chop_prefix ~prefix:ap1 ap2 |> Option.is_some
let replace_prefix ~prefix access_path replace_with =
match chop_prefix ~prefix access_path with
| Some remaining_accesses ->
Some (append replace_with remaining_accesses)
| None ->
None
end end
module Abs = struct module Abs = struct

@ -59,6 +59,8 @@ val append : t -> access list -> t
val is_prefix : t -> t -> bool val is_prefix : t -> t -> bool
(** return true if [ap1] is a prefix of [ap2]. returns true for equal access paths *) (** return true if [ap1] is a prefix of [ap2]. returns true for equal access paths *)
val replace_prefix : prefix:t -> t -> t -> t option [@@warning "-32"]
val inner_class_normalize : t -> t val inner_class_normalize : t -> t
(** transform an access path that starts on "this" of an inner class but which breaks out to (** transform an access path that starts on "this" of an inner class but which breaks out to
access outer class fields to the outermost one. access outer class fields to the outermost one.
@ -80,6 +82,8 @@ val pp_base : Format.formatter -> base -> unit
val pp_access : Format.formatter -> access -> unit val pp_access : Format.formatter -> access -> unit
val pp_access_list : Format.formatter -> access list -> unit [@@warning "-32"]
module Abs : sig module Abs : sig
type raw = t type raw = t

@ -15,11 +15,11 @@ type t =
; class_loads: ClassLoadsDomain.summary option ; class_loads: ClassLoadsDomain.summary option
; cost: CostDomain.summary option ; cost: CostDomain.summary option
; crashcontext_frame: Stacktree_t.stacktree option ; crashcontext_frame: Stacktree_t.stacktree option
; lab_resource_leaks: ResourceLeakDomain.summary option
; litho: LithoDomain.t option ; litho: LithoDomain.t option
; purity: PurityDomain.summary option ; purity: PurityDomain.summary option
; quandary: QuandarySummary.t option ; quandary: QuandarySummary.t option
; racerd: RacerDDomain.summary option ; racerd: RacerDDomain.summary option
; resources: ResourceLeakDomain.summary option
; siof: SiofDomain.Summary.t option ; siof: SiofDomain.Summary.t option
; starvation: StarvationDomain.summary option ; starvation: StarvationDomain.summary option
; typestate: TypeState.t option ; typestate: TypeState.t option
@ -32,6 +32,7 @@ let pp pe fmt
; class_loads ; class_loads
; cost ; cost
; crashcontext_frame ; crashcontext_frame
; lab_resource_leaks
; litho ; litho
; purity ; purity
; quandary ; quandary
@ -46,7 +47,7 @@ let pp pe fmt
| None -> | None ->
() ()
in in
F.fprintf fmt "%a%a%a%a%a%a%a%a%a%a%a%a%a%a@\n" F.fprintf fmt "%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a@\n"
(pp_opt "Biabduction" (BiabductionSummary.pp pe)) (pp_opt "Biabduction" (BiabductionSummary.pp pe))
biabduction (pp_opt "TypeState" TypeState.pp) typestate biabduction (pp_opt "TypeState" TypeState.pp) typestate
(pp_opt "ClassLoads" ClassLoadsDomain.pp_summary) (pp_opt "ClassLoads" ClassLoadsDomain.pp_summary)
@ -71,6 +72,8 @@ let pp pe fmt
starvation starvation
(pp_opt "Purity" PurityDomain.pp_summary) (pp_opt "Purity" PurityDomain.pp_summary)
purity purity
(pp_opt "Resource Leaks Lab" ResourceLeakDomain.pp)
lab_resource_leaks
let empty = let empty =
@ -84,7 +87,7 @@ let empty =
; purity= None ; purity= None
; quandary= None ; quandary= None
; racerd= None ; racerd= None
; resources= None ; lab_resource_leaks= None
; siof= None ; siof= None
; starvation= None ; starvation= None
; typestate= None ; typestate= None

@ -15,11 +15,11 @@ type t =
; class_loads: ClassLoadsDomain.summary option ; class_loads: ClassLoadsDomain.summary option
; cost: CostDomain.summary option ; cost: CostDomain.summary option
; crashcontext_frame: Stacktree_t.stacktree option ; crashcontext_frame: Stacktree_t.stacktree option
; lab_resource_leaks: ResourceLeakDomain.summary option
; litho: LithoDomain.t option ; litho: LithoDomain.t option
; purity: PurityDomain.summary option ; purity: PurityDomain.summary option
; quandary: QuandarySummary.t option ; quandary: QuandarySummary.t option
; racerd: RacerDDomain.summary option ; racerd: RacerDDomain.summary option
; resources: ResourceLeakDomain.summary option
; siof: SiofDomain.Summary.t option ; siof: SiofDomain.Summary.t option
; starvation: StarvationDomain.summary option ; starvation: StarvationDomain.summary option
; typestate: TypeState.t option ; typestate: TypeState.t option

@ -0,0 +1,23 @@
(*
* Copyright (c) 2017-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
type t = unit
let ( <= ) ~lhs:_ ~rhs:_ = assert false
let join _a _b = assert false
let widen ~prev:_ ~next:_ ~num_iters:_ = assert false
let pp fmt () = F.fprintf fmt "(nothing)"
let initial = ()
type summary = t

@ -0,0 +1,14 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
type summary = t

@ -0,0 +1,102 @@
(*
* Copyright (c) 2017-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
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct
type t = ResourceLeakDomain.t
let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with lab_resource_leaks= Some resources_payload}
let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ResourceLeakDomain
type extras = unit
let is_closeable_typename tenv typename =
let is_closable_interface typename _ =
match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" ->
true
| _ ->
false
in
PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ ->
false
let _acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *)
Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
let _releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *)
String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
(** Take an abstract state and instruction, produce a new abstract state *)
let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc= _; tenv= _} _
(instr : HilInstr.t) =
match instr with
| Call (_return_opt, Direct _callee_procname, _actuals, _, _loc) ->
(* function call [return_opt] := invoke [callee_procname]([actuals]) *)
astate
| Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *)
astate
| Assume (_assume_exp, _, _, _loc) ->
(* a conditional assume([assume_exp]). blocks if [assume_exp] evaluates to false *)
astate
| Call (_, Indirect _, _, _, _) ->
(* This should never happen in Java. Fail if it does. *)
L.(die InternalError) "Unexpected indirect call %a" HilInstr.pp instr
| ExitScope _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "resource leaks"
end
(** 5(a) Type of CFG to analyze--Exceptional to follow exceptional control-flow edges, Normal to
ignore them *)
module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak _post _summary (_proc_data : unit ProcData.t) = ()
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
let proc_data = ProcData.make proc_desc tenv () in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post ->
report_if_leak post summary proc_data ;
Payload.update_summary post summary
| None ->
L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp
(Procdesc.get_proc_name proc_data.pdesc)

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -0,0 +1,29 @@
(*
* Copyright (c) 2017-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
type t = int
let ( <= ) ~lhs ~rhs = lhs <= rhs
let join _a _b = assert false
let widen ~prev:_ ~next:_ ~num_iters:_ = assert false
let pp fmt astate = F.fprintf fmt "%d" astate
let initial = 0
let acquire_resource held = held + 1
let release_resource held = held - 1
let has_leak held = held > 0
type summary = t

@ -0,0 +1,20 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
val acquire_resource : t -> t
val release_resource : t -> t
val has_leak : t -> bool
type summary = t

@ -0,0 +1,109 @@
(*
* Copyright (c) 2017-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
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct
type t = ResourceLeakDomain.t
let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with lab_resource_leaks= Some resources_payload}
let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ResourceLeakDomain
type extras = unit
let is_closeable_typename tenv typename =
let is_closable_interface typename _ =
match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" ->
true
| _ ->
false
in
PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ ->
false
let acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *)
Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
let releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *)
String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
(** Take an abstract state and instruction, produce a new abstract state *)
let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc= _; tenv} _ (instr : HilInstr.t) =
match instr with
| Call (_return_opt, Direct callee_procname, _actuals, _, _loc) ->
(* function call [return_opt] := invoke [callee_procname]([actuals]) *)
if acquires_resource tenv callee_procname then ResourceLeakDomain.acquire_resource astate
else if releases_resource tenv callee_procname then
ResourceLeakDomain.release_resource astate
else astate
| Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *)
astate
| Assume (_assume_exp, _, _, _loc) ->
(* a conditional assume([assume_exp]). blocks if [assume_exp] evaluates to false *)
astate
| Call (_, Indirect _, _, _, _) ->
(* This should never happen in Java. Fail if it does. *)
L.(die InternalError) "Unexpected indirect call %a" HilInstr.pp instr
| ExitScope _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "resource leaks"
end
(** 5(a) Type of CFG to analyze--Exceptional to follow exceptional control-flow edges, Normal to
ignore them *)
module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak post summary (proc_data : unit ProcData.t) =
if ResourceLeakDomain.has_leak post then
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %a resource(s)" ResourceLeakDomain.pp post in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
let proc_data = ProcData.make proc_desc tenv () in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post ->
report_if_leak post summary proc_data ;
Payload.update_summary post summary
| None ->
L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp
(Procdesc.get_proc_name proc_data.pdesc)

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -0,0 +1,29 @@
(*
* Copyright (c) 2017-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
type t = int
let ( <= ) ~lhs ~rhs = lhs <= rhs
let join a b = max a b
let widen ~prev:_ ~next:_ ~num_iters:_ = assert false
let pp fmt astate = F.fprintf fmt "%d" astate
let initial = 0
let acquire_resource held = held + 1
let release_resource held = held - 1
let has_leak held = held > 0
type summary = t

@ -0,0 +1,20 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
val acquire_resource : t -> t
val release_resource : t -> t
val has_leak : t -> bool
type summary = t

@ -0,0 +1,109 @@
(*
* Copyright (c) 2017-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
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct
type t = ResourceLeakDomain.t
let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with lab_resource_leaks= Some resources_payload}
let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ResourceLeakDomain
type extras = unit
let is_closeable_typename tenv typename =
let is_closable_interface typename _ =
match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" ->
true
| _ ->
false
in
PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ ->
false
let acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *)
Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
let releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *)
String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
(** Take an abstract state and instruction, produce a new abstract state *)
let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc= _; tenv} _ (instr : HilInstr.t) =
match instr with
| Call (_return_opt, Direct callee_procname, _actuals, _, _loc) ->
(* function call [return_opt] := invoke [callee_procname]([actuals]) *)
if acquires_resource tenv callee_procname then ResourceLeakDomain.acquire_resource astate
else if releases_resource tenv callee_procname then
ResourceLeakDomain.release_resource astate
else astate
| Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *)
astate
| Assume (_assume_exp, _, _, _loc) ->
(* a conditional assume([assume_exp]). blocks if [assume_exp] evaluates to false *)
astate
| Call (_, Indirect _, _, _, _) ->
(* This should never happen in Java. Fail if it does. *)
L.(die InternalError) "Unexpected indirect call %a" HilInstr.pp instr
| ExitScope _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "resource leaks"
end
(** 5(a) Type of CFG to analyze--Exceptional to follow exceptional control-flow edges, Normal to
ignore them *)
module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak post summary (proc_data : unit ProcData.t) =
if ResourceLeakDomain.has_leak post then
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %a resource(s)" ResourceLeakDomain.pp post in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
let proc_data = ProcData.make proc_desc tenv () in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post ->
report_if_leak post summary proc_data ;
Payload.update_summary post summary
| None ->
L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp
(Procdesc.get_proc_name proc_data.pdesc)

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -0,0 +1,53 @@
(*
* Copyright (c) 2017-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 FiniteBounds = struct
type t = int
let ( <= ) ~lhs ~rhs = lhs <= rhs
let join a b = max a b
let widen ~prev ~next ~num_iters:_ = join prev next
let pp fmt astate = F.fprintf fmt "%d" astate
end
include AbstractDomain.TopLifted (FiniteBounds)
open AbstractDomain.Types
let widening_threshold = 5
let widen ~prev ~next ~num_iters =
match (prev, next) with
| Top, _ | _, Top ->
Top
| NonTop prev, NonTop next when num_iters < widening_threshold ->
NonTop (FiniteBounds.join prev next)
| NonTop _, NonTop _ (* num_iters >= widening_threshold *) ->
Top
let initial = NonTop 0
let acquire_resource = function Top -> Top | NonTop held -> NonTop (held + 1)
let release_resource = function Top -> Top | NonTop held -> NonTop (held - 1)
let has_leak = function
| Top ->
(* UNSOUND but likely that the analyzer got confused *) false
| NonTop x when x > 0 ->
true
| NonTop _ ->
false
type summary = t

@ -0,0 +1,20 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
val acquire_resource : t -> t
val release_resource : t -> t
val has_leak : t -> bool
type summary = t

@ -0,0 +1,109 @@
(*
* Copyright (c) 2017-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
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct
type t = ResourceLeakDomain.t
let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with lab_resource_leaks= Some resources_payload}
let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ResourceLeakDomain
type extras = unit
let is_closeable_typename tenv typename =
let is_closable_interface typename _ =
match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" ->
true
| _ ->
false
in
PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ ->
false
let acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *)
Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
let releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *)
String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
(** Take an abstract state and instruction, produce a new abstract state *)
let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc= _; tenv} _ (instr : HilInstr.t) =
match instr with
| Call (_return_opt, Direct callee_procname, _actuals, _, _loc) ->
(* function call [return_opt] := invoke [callee_procname]([actuals]) *)
if acquires_resource tenv callee_procname then ResourceLeakDomain.acquire_resource astate
else if releases_resource tenv callee_procname then
ResourceLeakDomain.release_resource astate
else astate
| Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *)
astate
| Assume (_assume_exp, _, _, _loc) ->
(* a conditional assume([assume_exp]). blocks if [assume_exp] evaluates to false *)
astate
| Call (_, Indirect _, _, _, _) ->
(* This should never happen in Java. Fail if it does. *)
L.(die InternalError) "Unexpected indirect call %a" HilInstr.pp instr
| ExitScope _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "resource leaks"
end
(** 5(a) Type of CFG to analyze--Exceptional to follow exceptional control-flow edges, Normal to
ignore them *)
module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak post summary (proc_data : unit ProcData.t) =
if ResourceLeakDomain.has_leak post then
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %a resource(s)" ResourceLeakDomain.pp post in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
let proc_data = ProcData.make proc_desc tenv () in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post ->
report_if_leak post summary proc_data ;
Payload.update_summary post summary
| None ->
L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp
(Procdesc.get_proc_name proc_data.pdesc)

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -0,0 +1,86 @@
(*
* Copyright (c) 2017-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 FiniteBounds = struct
type t = int
let ( <= ) ~lhs ~rhs = lhs <= rhs
let join a b = max a b
let widen ~prev ~next ~num_iters:_ = join prev next
let pp fmt astate = F.fprintf fmt "%d" astate
end
module BoundsWithTop = struct
open AbstractDomain.Types
include AbstractDomain.TopLifted (FiniteBounds)
let widening_threshold = 5
let widen ~prev ~next ~num_iters =
match (prev, next) with
| Top, _ | _, Top ->
Top
| NonTop prev, NonTop next when num_iters < widening_threshold ->
NonTop (FiniteBounds.join prev next)
| NonTop _, NonTop _ (* num_iters >= widening_threshold *) ->
Top
end
module ReturnsResource = AbstractDomain.BooleanOr
include AbstractDomain.Pair (BoundsWithTop) (ReturnsResource)
open AbstractDomain.Types
let initial = (NonTop 0, false)
let acquire_resource = function
| (Top, _) as astate ->
astate
| NonTop held, returns_resource ->
(NonTop (held + 1), returns_resource)
let release_resource = function
| (Top, _) as astate ->
astate
| NonTop held, returns_resource ->
(NonTop (held - 1), returns_resource)
let has_leak = function
| Top, _ ->
(* UNSOUND but likely that the analyzer got confused *) false
| NonTop x, _ when x > 0 ->
true
| NonTop _, _ ->
false
let apply_summary ~summary:(summary_count, summary_returns_resource)
(current_count, current_returns_resource) =
let new_count =
match current_count with
| Top ->
Top
| NonTop current_count ->
let return_count = if summary_returns_resource then 1 else 0 in
let summary_count =
match summary_count with Top -> (* confusion => ignore *) 0 | NonTop count -> count
in
NonTop (current_count + summary_count + return_count)
in
(new_count, current_returns_resource)
let record_return_resource (count, _) = (count, true)
type summary = t

@ -0,0 +1,24 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
val acquire_resource : t -> t
val release_resource : t -> t
val has_leak : t -> bool
val apply_summary : summary:t -> t -> t
val record_return_resource : t -> t
type summary = t

@ -0,0 +1,123 @@
(*
* Copyright (c) 2017-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
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct
type t = ResourceLeakDomain.t
let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with lab_resource_leaks= Some resources_payload}
let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ResourceLeakDomain
type extras = unit
let is_closeable_typename tenv typename =
let is_closable_interface typename _ =
match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" ->
true
| _ ->
false
in
PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ ->
false
let acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *)
Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
let releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *)
String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
(** Take an abstract state and instruction, produce a new abstract state *)
let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc; tenv} _ (instr : HilInstr.t) =
match instr with
| Call (_return_opt, Direct callee_procname, _actuals, _, _loc) -> (
if
(* function call [return_opt] := invoke [callee_procname]([actuals]) *)
acquires_resource tenv callee_procname
then ResourceLeakDomain.acquire_resource astate
else if releases_resource tenv callee_procname then
ResourceLeakDomain.release_resource astate
else
match Payload.read pdesc callee_procname with
| Some summary ->
(* interprocedural analysis produced a summary: use it *)
ResourceLeakDomain.apply_summary ~summary astate
| None ->
(* No summary for [callee_procname]; it's native code or missing for some reason *)
astate )
| Assign
(Base (ret, {Typ.desc= Typ.Tptr ({Typ.desc= Typ.Tstruct ret_typename}, _)}), _rhs_exp, _loc)
when Var.is_return ret && is_closeable_typename tenv ret_typename ->
(* return a resource *)
ResourceLeakDomain.record_return_resource astate |> ResourceLeakDomain.release_resource
| Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *)
astate
| Assume (_assume_exp, _, _, _loc) ->
(* a conditional assume([assume_exp]). blocks if [assume_exp] evaluates to false *)
astate
| Call (_, Indirect _, _, _, _) ->
(* This should never happen in Java. Fail if it does. *)
L.(die InternalError) "Unexpected indirect call %a" HilInstr.pp instr
| ExitScope _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "resource leaks lab"
end
(** 5(a) Type of CFG to analyze--Exceptional to follow exceptional control-flow edges, Normal to
ignore them *)
module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak post summary (proc_data : unit ProcData.t) =
if ResourceLeakDomain.has_leak post then
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %a resource(s)" ResourceLeakDomain.pp post in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
let proc_data = ProcData.make proc_desc tenv () in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post ->
report_if_leak post summary proc_data ;
Payload.update_summary post summary
| None ->
L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp
(Procdesc.get_proc_name proc_data.pdesc)

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -0,0 +1,179 @@
(*
* Copyright (c) 2017-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 FiniteBounds = struct
type t = int
let ( <= ) ~lhs ~rhs = lhs <= rhs
let join a b = max a b
let widen ~prev ~next ~num_iters:_ = join prev next
let pp fmt astate = F.fprintf fmt "%d" astate
end
module BoundsWithTop = struct
open AbstractDomain.Types
include AbstractDomain.TopLifted (FiniteBounds)
let widening_threshold = 5
let widen ~prev ~next ~num_iters =
match (prev, next) with
| Top, _ | _, Top ->
Top
| NonTop prev, NonTop next when num_iters < widening_threshold ->
NonTop (FiniteBounds.join prev next)
| NonTop _, NonTop _ (* num_iters >= widening_threshold *) ->
Top
end
module ResourcesHeld = AbstractDomain.Map (AccessPath) (BoundsWithTop)
open AbstractDomain.Types
let initial = ResourcesHeld.empty
let update_count count n = match count with Top -> Top | NonTop held -> NonTop (held + n)
let incr_count count = update_count count 1
let decr_count count = update_count count (-1)
let find_count access_path held =
match ResourcesHeld.find_opt access_path held with Some count -> count | None -> NonTop 0
let acquire_resource access_path held =
let old_count = find_count access_path held in
ResourcesHeld.add access_path (incr_count old_count) held
let release_resource access_path held =
let old_count = find_count access_path held in
ResourcesHeld.add access_path (decr_count old_count) held
let assign lhs_access_path rhs_access_path held =
let one_binding access_path count held =
match AccessPath.replace_prefix ~prefix:rhs_access_path lhs_access_path access_path with
| Some base_access_path ->
ResourcesHeld.add base_access_path count held
| None ->
ResourcesHeld.add access_path count held
in
ResourcesHeld.fold one_binding held ResourcesHeld.empty
let has_leak formal_map held =
(* test if we acquired resources that we do not return to the caller *)
let is_local_leak access_path count =
let base, _ = access_path in
match (count, base) with
| Top, _ ->
false
| NonTop count, _ when count > 1 ->
true
| NonTop count, _ when count <= 0 ->
false
(* count = 1 *)
| _, (var, _) when Var.is_global var ->
false
| _, (ret, _) when Var.is_return ret ->
false
| _, base when FormalMap.is_formal base formal_map ->
false
| _ ->
true
in
ResourcesHeld.exists is_local_leak held
module Summary = struct
module InterfaceAccessPath = struct
type base = Return | Formal of int [@@deriving compare]
let pp_base f = function
| Return ->
F.pp_print_string f "Return"
| Formal i ->
F.fprintf f "Formal(%d)" i
type t = base * AccessPath.access list [@@deriving compare]
let pp f = function
| base, [] ->
pp_base f base
| base, accesses ->
F.fprintf f "%a.%a" pp_base base AccessPath.pp_access_list accesses
end
module ResourcesFromFormals = PrettyPrintable.MakePPMap (InterfaceAccessPath)
type t = BoundsWithTop.t ResourcesFromFormals.t
let pp = ResourcesFromFormals.pp ~pp_value:BoundsWithTop.pp
let make formal_map held =
let to_interface access_path =
let base, accesses = access_path in
match FormalMap.get_formal_index base formal_map with
| Some i ->
Some (InterfaceAccessPath.Formal i, accesses)
| None ->
if Var.is_return (fst base) then Some (InterfaceAccessPath.Return, accesses) else None
in
ResourcesHeld.fold
(fun access_path count acquired ->
match to_interface access_path with
| Some interface_access_path ->
ResourcesFromFormals.add interface_access_path count acquired
| None ->
acquired )
held ResourcesFromFormals.empty
let apply ~summary ~return ~actuals held =
let apply_one (base, accesses) callee_count held =
let access_path_opt =
match (base : InterfaceAccessPath.base) with
| Return ->
Some (return, accesses)
| Formal i -> (
match List.nth actuals i with
| Some (HilExp.AccessExpression actual_expr) ->
Some
(AccessPath.append (HilExp.AccessExpression.to_access_path actual_expr) accesses)
| _ ->
None )
in
match access_path_opt with
| None ->
held
| Some access_path ->
let new_count =
match callee_count with
| Top ->
Top
| NonTop callee_count ->
let old_count =
ResourcesHeld.find_opt access_path held |> Option.value ~default:(NonTop 0)
in
update_count old_count callee_count
in
ResourcesHeld.add access_path new_count held
in
ResourcesFromFormals.fold apply_one summary held
end
type summary = Summary.t
include ResourcesHeld

@ -0,0 +1,30 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
val acquire_resource : AccessPath.t -> t -> t
val release_resource : AccessPath.t -> t -> t
val assign : AccessPath.t -> AccessPath.t -> t -> t
val has_leak : FormalMap.t -> t -> bool
type summary
module Summary : sig
val apply : summary:summary -> return:AccessPath.base -> actuals:HilExp.t list -> t -> t
val make : FormalMap.t -> t -> summary
val pp : Format.formatter -> summary -> unit
end

@ -0,0 +1,131 @@
(*
* Copyright (c) 2017-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
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct
type t = ResourceLeakDomain.summary
let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with lab_resource_leaks= Some resources_payload}
let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end)
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = ResourceLeakDomain
type extras = unit
let is_closeable_typename tenv typename =
let is_closable_interface typename _ =
match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" ->
true
| _ ->
false
in
PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ ->
false
let acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *)
Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
let releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *)
String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
(** Take an abstract state and instruction, produce a new abstract state *)
let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc; tenv} _ (instr : HilInstr.t) =
match instr with
| Call (_return, Direct callee_procname, HilExp.AccessExpression allocated :: _, _, _loc)
when acquires_resource tenv callee_procname ->
ResourceLeakDomain.acquire_resource
(HilExp.AccessExpression.to_access_path allocated)
astate
| Call (_, Direct callee_procname, [actual], _, _loc)
when releases_resource tenv callee_procname -> (
match actual with
| HilExp.AccessExpression access_expr ->
ResourceLeakDomain.release_resource
(HilExp.AccessExpression.to_access_path access_expr)
astate
| _ ->
astate )
| Call (return, Direct callee_procname, actuals, _, _loc) -> (
match Payload.read pdesc callee_procname with
| Some summary ->
(* interprocedural analysis produced a summary: use it *)
ResourceLeakDomain.Summary.apply ~summary ~return ~actuals astate
| None ->
(* No summary for [callee_procname]; it's native code or missing for some reason *)
astate )
| Assign (access_expr, AccessExpression rhs_access_expr, _loc) ->
ResourceLeakDomain.assign
(HilExp.AccessExpression.to_access_path access_expr)
(HilExp.AccessExpression.to_access_path rhs_access_expr)
astate
| Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *)
astate
| Assume (_assume_exp, _, _, _loc) ->
(* a conditional assume([assume_exp]). blocks if [assume_exp] evaluates to false *)
astate
| Call (_, Indirect _, _, _, _) ->
(* This should never happen in Java. Fail if it does. *)
L.(die InternalError) "Unexpected indirect call %a" HilInstr.pp instr
| ExitScope _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "resource leaks lab"
end
(** 5(a) Type of CFG to analyze--Exceptional to follow exceptional control-flow edges, Normal to
ignore them *)
module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak post summary formal_map (proc_data : unit ProcData.t) =
if ResourceLeakDomain.has_leak formal_map post then
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %a resource(s)" ResourceLeakDomain.pp post in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
let proc_data = ProcData.make proc_desc tenv () in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post ->
let formal_map = FormalMap.make proc_desc in
report_if_leak post summary formal_map proc_data ;
Payload.update_summary (ResourceLeakDomain.Summary.make formal_map post) summary
| None ->
L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp
(Procdesc.get_proc_name proc_data.pdesc)

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -0,0 +1,150 @@
# Build your own Resource Leak analysis
This is a lab exercise designed to take the participant through the basics of using the Infer.AI framework for building compositional abstract interpreters. We provide the skeleton for a simple intraprocedural resource leak analysis. During this exercise, you will identify limitations in the existing approach and work on extending it to a create a more powerful interprocedural analysis.
The files to work on are [ResourceLeaks.ml](./ResourceLeaks.ml) and [ResourceLeakDomain.ml](./ResourceLeakDomain.ml), and their corresponding .mli files.
## (0) Quick Start
### (a) With Docker...
Using Docker is the fastest way: you do not need to clone the Infer repository and every thing is set up in the Docker image for you to start hacking on Infer straight away.
1. Get Docker: https://www.docker.com/get-started
2. Run the infer Docker image with `docker run -it infer/infer:infer-latest-java-dev -v $HOME/infer-docker:/infer-host /bin/bash`. This will give you a prompt *inside the Docker image*. Do not close that terminal for the duration of the lab.
3. Within Docker, copy the /infer directory to your mount point: `cp -av /infer/. /infer-host`
4. You can now edit the files *locally* in `$HOME/infer-docker` and build infer *inside Docker* in `/infer-host` using the shell prompt gotten in step 2. The quickest way is to run `make byte` from `/infer-host`.
### (a') ...or, alternatively, build Infer locally from scratch
Clone the Infer repository at https://github.com/facebook/infer and read the instructions in [INSTALL.md](https://github.com/facebook/infer/blob/master/INSTALL.md) to build Infer from source. Note that you only need Infer for Java for this lab: `./build-infer.sh java`.
### (b) Set up your Development Environment
See [CONTRIBUTING.md](https://github.com/facebook/infer/blob/master/CONTRIBUTING.md#hacking-on-the-code) to set your editor and for tips and tricks on how to hack on Infer more efficiently. One of the most useful things to install in your editor to navigate OCaml source code efficiently is [Merlin](https://github.com/ocaml/merlin/wiki).
## (1) Warm up: running, testing, and debugging Infer
(a) Change to the test directory (`cd infer/tests/codetoanalyze/java/lab`) and run the checker on a single file:
```
infer --resource-leak-only -- javac Leaks.java
```
Infer should report 3 bugs.
(b) Run the analyzer on a single test file to produce the debug HTML:
```
infer -g --resource-leak-only -- javac Leaks.java
```
Then, open the debug HTML:
```
firefox infer-out/captured/*.html
```
This helpful artifact shows the Infer warnings alongside the code they are complaining about. It also displays the CFG node(s) associated with each line of code. Clicking a CFG node shows the Infer IR associated with the node, and the pre/post state computed while analyzing the instruction. Come back to the debug HTML early and often when you can't understand what your analysis is doing--it will help!
(c) The `Logging.d_printf`/`Logging.d_printfln` functions print to the debug HTML. The logging is contextual: it will print to the log for the CFG node that's being analyzed at the time the logging statement execute. Try adding `Logging.d_printfln "Hi Infer";` inside of the case for `Call`, recompiling/re-running. You should see the text you printed inside the appropriate CFG node log in the debug HTML.
(d) The `Logging.debug_dev` function prints to the console. This can be useful for printing information that doesn't happen in the context of a particular CFG node (e.g., performing post-processing on a summary). Try adding `Logging.debug_dev "Hi Infer;"` to the `compute_post` function, recompile/re-run and see your text printed on the command line.
## (2) Integer domain for straight-line programs
(a) Let's start with a simple-minded analysis: count the number of open resources. Change `ResourceLeakDomain.t` to be of type `int`:
```OCaml
type t = int
```
You don't need to change `join` or `widen` yet, this will be done later. You also don't need to change ResourceLeakDomain.mli, only ResourceLeakDomain.ml.
(b) Now, in `ResourceLeaks.ml`, change the first `Call` case of `exec_instr` to bump the integer when a resource is acquired, and decrease it when a resource is released. Use `acquires_resource` (remove the leading `_`, telling OCaml not to warn that it was unused) and `releases_resource` (same). For the rest of the lab, it will be useful **not** to expose the type of `ResourceLeakDomain.t` to `ResourceLeaks.ml`, so add functions `ResourceLeakDomain.acquire_resource` and `ResourceLeakDomain.release_resource` to do the actual integer manipulations. Expose these functions in ResourceLeakDomain.mli`.
Finally, look again at the HTML debug output of infer on [Leaks.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/Leaks.java). You should see the resource count be incremented and decremented appropriately.
(c) Now let's report leaks! Write and expose a function `ResourceLeakDomain.has_leak`, true when an abstract state shows a leak. Then change `ResourceLeaks.report_if_leak` to report when `ResourceLeakDomain.has_leak post` is true. You can use this code to report:
```OCaml
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %d resource(s)" ResourceLeakDomain.pp leak_count in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
```
(d) Think about the concretization of the resource count. What does a resource count of zero mean? Is there a concrete state in the concretization of "Resource count zero" that leaks a resource? Write a simple test method `FN_leakBad` in [Leaks.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/Leaks.java) that will produce this concrete state (that is, a false negative test where the program leaks a resource, but the analyzer doesn't catch it).
(e) In addition, there are programs that do not leak resources that the analyzer will flag. Write a simple test method `FP_noLeakOk` that exhibits this problem (that is, a false positive test that demonstrates the imprecision of the analyzer).
(f) Do your false negative and false positive examples have a common underlying cause? Can the domain be improved to address them?
Let's stick with just an integer domain to keep things simple until (5).
## (3) Integer domain for branching and looping programs
(a) Run your checker on [LeaksBranch.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/LeaksBranch.java) it again. Uh oh, it crashed! Fix it by implementing `join` for your domain.
(b) Now run on [LeaksLoop.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/LeaksLoop.java), and implement `widen`. Hmm... There's a termination bug in the abstract domain--do you see it?
(c) Time for a richer domain! A classic solution to this problem in abstract interpretation is to add a `Top` value that is larger than any integer.
- Fix the domain to make your test pass and ensure that the analyzer will always terminate. Make sure your fix is sufficiently general (remember: the allocation count can be negative :)).
- Hint: `AbstractDomain.TopLifted` may be useful for this. Just put all the code in ResourceLeakDomain.ml that corresponds to the signature `AbstractDomain.S` into a submodule `FiniteBounds`, then let `TopLifted` do all the lifting with
```
include AbstractDomain.TopLifter (FiniteBounds)
```
- Hint#2: use `open AbstractDomain.Types` to be able to write, e.g., `Top` instead of `AbstractDomain.Top`.
## (4) Interprocedural analysis
Augment the summary type with state to indicate when the current procedure returns a resource. Allowing a resource to escape to the caller should not be considered a leak. This improvement should allow your analysis to pass the tests in [LeaksInterprocedural.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/LeaksInterprocedural.java).
Hint: What do return values look like in infer? They are assignments to a special variable, detected by `Var.is_return`. You may also want to match only return variables of some specific object type. Use this code to look at the classname of the type of the return value:
```OCaml
| Assign
(Base (ret, {Typ.desc= Typ.Tptr ({Typ.desc= Typ.Tstruct ret_typename}, _)}), _rhs_exp, _loc)
when Var.is_return ret ->
```
Then `ret_var` is the return variable (if `Var.is_return ret_var` returns true), of type `ret_typename` (really `ret_typename*` in infer's intermediate representation).
## (5) Access paths
(a) Change the simple counting domain to a domain that overapproximates the set of storage locations that hold a resource. As a concrete goal, the new domain should allow you to print the name of the resource(s) that leak in the error message (rather than just the number of resources). The new domain should also allow your analysis to get the correct answer on your false negative and false positive tests from 2(d) and 2(e). Think about the following questions when designing your new domain:
- How should we abstract storage locations? Is abstracting the stack program variables (`Var.t`)'s enough, or do we need an abstraction of the heap as well?
- How will we handle aliasing (storage locations that store the same address)? More precisely, how to handle assignement?
- Will it be easy to extend the domain to incorporate information from the callee summaries/represent state that will be instantiated by callers?
- Some modules that might be useful in creating your new domain (depending on what approach you choose): `AbstractDomain.FiniteSet`, `AbstractDomain.Map`, `AccessPath`, `Var`.
- It's okay for the domain to diverge in certain cases for the purpose of this lab. Can you write a method that would make your checker diverge?
(b) Write some tests that demonstrate the limitations of your new domain: both false positives (names prefixed with `FP_` and false negatives (prefixed with `FN_`). Add them to [LeaksAccessPaths.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/LeaksAccessPath.java).
(c) Augment the summary type with state to indicate formals that are closed by the current function. This should allow your analysis to pass the tests in [LeaksAccessPathsInterprocedural.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/LeaksAccessPathsInterprocedural.java).
Hint: You will find the `FormalMap` module useful for this. This module lets you go back and forth between the index of a formal and its name. This utility module is also used in the `RacerD` and `TaintAnalysis` modules.
## (6) Making it practical
(a) Real resource leaks frequently involve failing to close resources along exceptional control-flow paths. For simplicity, the initial version of the current analysis uses a filtered view of the CFG that skips exceptional edges (`ProcCfg.Normal`). To find more bugs, you might want to switch to using `ProcCfg.Exceptional` and make sure that your analysis gets the right answer on some realistic exception examples like [LeaksExceptions.java](https://github.com/facebook/infer/blob/master/infer/tests/codetoanalyze/java/lab/LeaksExceptions.java).
(b) Try running on real code! The instructions [here](http://fm.csl.sri.com/SSFT17/infer-instr.html) have several suggestions for open-source Android apps to point your analysis at. Try `./gradlew assembleDebug -x test` first to make sure everything builds correctly without Infer (if not, you are probably missing some dependencies--the error messages should guide you). Once that's working, try
`./gradlew clean; infer run --resource-leak-only -- ./gradlew assembleDebug -x test`.
- Found a real bug? Bonus points! Send a pull request to fix it! Very frequently, the best fix is to use try-with-resources.
- Found a false positive in your analysis? Try re-running Infer with `--debug` and see if you can narrow down the root cause/fix it.
- How does your analysis compare to Infer's production resource leak analysis? Run with `infer -- <gradle command>` to see if your analysis finds bugs that Infer misses, or vice versa.

@ -8,23 +8,16 @@
open! IStd open! IStd
module F = Format module F = Format
(* Extremely simple abstraction of resources: count the number of acquired resources. If there's type t = unit
not a corresponding number of releases, there may be a leak. *)
type t = int
(* 2(a) *) let ( <= ) ~lhs:_ ~rhs:_ = assert false
(* For now, type of abstract state and summary are the same *)
type summary = t
(* 4(a) *)
let ( <= ) ~lhs ~rhs = lhs <= rhs let join _a _b = assert false
let join = Pervasives.max let widen ~prev:_ ~next:_ ~num_iters:_ = assert false
let widen ~prev ~next ~num_iters:_ = join prev next let pp fmt () = F.fprintf fmt "(nothing)"
let pp fmt astate = F.fprintf fmt "Resource count: %d" astate let initial = ()
(* At the beginning of a procedure, assume no resources are held *) type summary = t
let initial = 0

@ -0,0 +1,14 @@
(*
* Copyright (c) 2017-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
include AbstractDomain.S
val initial : t
type summary = t

@ -8,32 +8,25 @@
open! IStd open! IStd
module F = Format module F = Format
module L = Logging module L = Logging
module Domain = ResourceLeakDomain
(* Boilerplate to write/read our summaries alongside the summaries of other analyzers *) (* Boilerplate to write/read our summaries alongside the summaries of other analyzers *)
module Payload = SummaryPayload.Make (struct module Payload = SummaryPayload.Make (struct
type t = Domain.t type t = ResourceLeakDomain.t
let update_payloads resources_payload (payloads : Payloads.t) = let update_payloads resources_payload (payloads : Payloads.t) =
{payloads with resources= Some resources_payload} {payloads with lab_resource_leaks= Some resources_payload}
let of_payloads (payloads : Payloads.t) = payloads.resources let of_payloads {Payloads.lab_resource_leaks} = lab_resource_leaks
end) end)
type extras = FormalMap.t
module TransferFunctions (CFG : ProcCfg.S) = struct module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG module CFG = CFG
module Domain = Domain module Domain = ResourceLeakDomain
type nonrec extras = extras type extras = unit
(* Take an abstract state and instruction, produce a new abstract state *) let is_closeable_typename tenv typename =
let exec_instr (astate : Domain.t) {ProcData.pdesc; tenv} _ (instr : HilInstr.t) =
let is_closeable procname tenv =
match procname with
| Typ.Procname.Java java_procname ->
let is_closable_interface typename _ = let is_closable_interface typename _ =
match Typ.Name.name typename with match Typ.Name.name typename with
| "java.io.AutoCloseable" | "java.io.Closeable" -> | "java.io.AutoCloseable" | "java.io.Closeable" ->
@ -41,41 +34,35 @@ module TransferFunctions (CFG : ProcCfg.S) = struct
| _ -> | _ ->
false false
in in
PatternMatch.supertype_exists tenv is_closable_interface PatternMatch.supertype_exists tenv is_closable_interface typename
let is_closeable_procname tenv procname =
match procname with
| Typ.Procname.Java java_procname ->
is_closeable_typename tenv
(Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname)) (Typ.Name.Java.from_string (Typ.Procname.Java.get_class_name java_procname))
| _ -> | _ ->
false false
in
let _acquires_resource tenv procname =
(* We assume all constructors of a subclass of Closeable acquire a resource *) (* We assume all constructors of a subclass of Closeable acquire a resource *)
let acquires_resource procname tenv = Typ.Procname.is_constructor procname && is_closeable_procname tenv procname
Typ.Procname.is_constructor procname && is_closeable procname tenv
in
let _releases_resource tenv procname =
(* We assume the close method of a Closeable releases all of its resources *) (* We assume the close method of a Closeable releases all of its resources *)
let releases_resource procname tenv = String.equal "close" (Typ.Procname.get_method procname) && is_closeable_procname tenv procname
match Typ.Procname.get_method procname with
| "close" ->
is_closeable procname tenv (** Take an abstract state and instruction, produce a new abstract state *)
| _ -> let exec_instr (astate : ResourceLeakDomain.t) {ProcData.pdesc= _; tenv= _} _
false (instr : HilInstr.t) =
in
match instr with match instr with
| Call (_return_opt, Direct callee_procname, _actuals, _, _loc) -> ( | Call (_return_opt, Direct _callee_procname, _actuals, _, _loc) ->
(* function call [return_opt] := invoke [callee_procname]([actuals]) *) (* function call [return_opt] := invoke [callee_procname]([actuals]) *)
(* 1(e) *) astate
let astate' =
if acquires_resource callee_procname tenv then astate + 1 (* 2(a) *)
else if releases_resource callee_procname tenv then astate - 1 (* 2(a) *)
else astate
in
match Payload.read pdesc callee_procname with
| Some _summary ->
(* Looked up the summary for callee_procname... do something with it *)
(* 4(a) *)
(* 5(b) *)
astate'
| None ->
(* No summary for callee_procname; it's native code or missing for some reason *)
astate' )
| Assign (_lhs_access_path, _rhs_exp, _loc) -> | Assign (_lhs_access_path, _rhs_exp, _loc) ->
(* an assignment [lhs_access_path] := [rhs_exp] *) (* an assignment [lhs_access_path] := [rhs_exp] *)
astate astate
@ -99,27 +86,16 @@ module CFG = ProcCfg.Normal
(* Create an intraprocedural abstract interpreter from the transfer functions we defined *) (* Create an intraprocedural abstract interpreter from the transfer functions we defined *)
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG)) module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (CFG))
(** Report an error when we have acquired more resources than we have released *)
let report_if_leak _post _summary (_proc_data : unit ProcData.t) = ()
(* Callback for invoking the checker from the outside--registered in RegisterCheckers *) (* Callback for invoking the checker from the outside--registered in RegisterCheckers *)
let checker {Callbacks.summary; proc_desc; tenv} : Summary.t = let checker {Callbacks.summary; proc_desc; tenv} : Summary.t =
(* Report an error when we have acquired more resources than we have released *) let proc_data = ProcData.make proc_desc tenv () in
let report leak_count (proc_data : extras ProcData.t) =
if leak_count > 0 (* 2(a) *) then
let last_loc = Procdesc.Node.get_loc (Procdesc.get_exit_node proc_data.pdesc) in
let message = F.asprintf "Leaked %d resource(s)" leak_count in
Reporting.log_error summary ~loc:last_loc IssueType.resource_leak message
in
(* Convert the abstract state to a summary. for now, just the identity function *)
let convert_to_summary (post : Domain.t) : Domain.summary =
(* 4(a) *)
post
in
let extras = FormalMap.make proc_desc in
let proc_data = ProcData.make proc_desc tenv extras in
match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with match Analyzer.compute_post proc_data ~initial:ResourceLeakDomain.initial with
| Some post -> | Some post ->
(* 1(f) *) report_if_leak post summary proc_data ;
report post proc_data ; Payload.update_summary post summary
Payload.update_summary (convert_to_summary post) summary
| None -> | None ->
L.(die InternalError) L.(die InternalError)
"Analyzer failed to compute post for %a" Typ.Procname.pp "Analyzer failed to compute post for %a" Typ.Procname.pp

@ -0,0 +1,10 @@
(*
* Copyright (c) 2017-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
val checker : Callbacks.proc_callback_t

@ -1,138 +0,0 @@
This is a lab exercise designed to take the participant through the basics of using the Infer.AI framework for building compositional abstract interpreters. We provide the skeleton for a simple intraprocedural resource leak analysis. During this exercise, you will identify limitations in the existing approach and work on extending it to a create a more powerful interprocedural analysis.
This document assumes that you have already [installed](https://github.com/facebook/infer/blob/master/INSTALL.md) Infer from source (you only need Infer for Java for this lab: `./build-infer.sh java`), successfully compiled it, and [set up](https://github.com/facebook/infer/blob/master/CONTRIBUTING.md#hacking-on-the-code) your OCaml dev environment.
The `infer/src/labs/ResourceLeaks.ml` and `infer/src/labls/ResourceLeaksDomain.ml` files have comments containing hints for which parts of the code you need to change in order to accomplish a particular task (e.g., 1(e) in `ResourceLeaks.ml`).
## (1) Warm up: running, testing, and debugging Infer
(a) Change to the test directory (`cd infer/tests/codetoanalyze/java/lab`) and run the checker on a single file: `infer --resource-leak-only -- javac Leaks.java`. Infer should report 3 bugs.
(b) Run the tests with `make test`. The tests work by producing an output file `issues.exp.test`, which is compared against the expected output file `issues.exp`. Tests pass when the two files are identical. You should see no output, which indicates that the tests have passed.
(c) Add a new test method containing a resource leak to `Leaks.java`, then run the tests again. The tests should now fail, and the error message should indicate that the failure is due to the new lreeak that you added. As the message suggests, run `make replace` to update the expected output (this simply copies the actual output `issues.exp.test` to the expected output `issues.exp`). Re-run the tests; they should now pass again.
(d) Run the analyzer on a single test file to produce the debug HTML: `infer --debug --resource-leak-only -- javac Leaks.java`. Then, open the debug HTML: `open infer-out/captured/*.html`. This helpful artifact shows the Infer warnings alongside the code they are complaining about. It also displays the CFG node(s) associated with each line of code. Clicking a CFG node shows the Infer IR associated with the node, and the pre/post state computed while analyzing the instruction. Come back to the debug HTML early and often when you can't understand what your analysis is doing--it will help!
(e) The `Logging.d_str`/`Logging.d_strln` functions print to the debug HTML. The logging is contextual: it will print to the log for the CFG node that's being analyzed at the time the logging statement execute. Try adding `Logging.d_strln "Hi Infer";` inside of the case for `Call`, recompiling/re-running. You should see the text you printed inside the appropriate CFG node log in the debug HTML.
(f) The `Logging.progress` function prints to stderr. This can be useful for printing information that doesn't happen in the context of a particular CFG node (e.g., performing post-processing on a summary). Try adding `Logging.progress "Hi Infer;"` to the `compute_post` function, recompile/re-run and see your text printed on the command line.
## (2) Reasoning about abstract domains
(a) Look at the `ResourceLeakDomain.ml`. There's a termination bug in the abstract domain--do you see it?.
- Write a test method that makes the analyzer diverge (Note: this may manifest as a stack overflow that crashes Infer rather than geniune divergence).
- Fix the domain to make your test pass and ensure that the analyzer will always terminate. Make sure your fix is sufficiently general (remember: the allocation count can be negative :)).
- Hint: `AbstractDomain.TopLifted` may be useful for this.
(b) Think about the concretization of the resource count. What does a resource count of zero mean? Is there a concrete state in the concretization of "Resource count zero" that leaks a resource? Write a simple test method `FN_leakBad` that will produce this concrete state (that is, a false negative test where the program leaks a resource, but the analyzer doesn't catch it)?
(c) In addition, there are programs that do not leak resources that the analyzer will flag. Write a simple test method `FP_noLeakOk` that exhibits this problem (that is, a false positive test that demonstrates the imprecision of the analyzer)?
(d) Do your false negative and false positive examples have a common underlying cause? Can the domain be improved to address them?
## (3) Improving the domain
(a) Change the simple counting domain to a domain that overapproximates the set of storage locations that hold a resource. As a concrete goal, the new domain should allow you to print the name of the resource(s) that leak in the error message (rather than just the number of resources). The new domain should also allow your analysis to get the correct answer on your false negative and false positive tests from 2(b) and 2(c). Think about the following questions when designing your new domain:
- How should we abstract storage locations? Is abstracting the stack program variables (`Var.t`)'s enough, or do we need an abstraction of the heap as well?
- How will we handle aliasing (storage locations that store the same address?)
- Will it be easy to extend the domain to incorporate information from the callee summaries/represent state that will be instantiated by callers (see 4)?
- Some modules that might be useful in creating your new domain (depending on what approach you choose): `AbstractDomain.FiniteSet`, `AbstractDomain.Map`, `AccessPath`, `Var`.
(b) Write some tests that demonstrate the limitations of your new domain: both false positives (names prefixed with `FP_` and false negatives (prefixed with `FN_`).
## (4) Interprocedural analysis
At this point, you likely have a fairly advanced intraprocedural analysis that is capable of finding resource leaks in real programs. Feel free to skip to 5 if you are eager to get started with bugfinding; many real resource leak bugs are intraprocedural. Alternatively, continue on with 4 to make your analysis interprocedural.
(a) Augment the summary type with state to indicate when the current procedure returns a resource. Allowing a resource to escape to the caller should not be considered a leak. This improvement should allow your analysis to pass the following tests:
```
FileInputStream returnResourceOk() {
return new FileInputStream("file.txt");
}
FileInputStream returnResourceWrapperOk() {
return returnResourceOk();
}
void returnResourceThenCloseOk() {
returnResourceWrapperOk().close();
}
void returnResourceThenLeakBad() {
returnResourceWrapperOk(); // warning
}
```
(b) Augment the summary type with state to indicate formals that are closed by the current function. This should allow your analysis to pass the following tests:
```
void closeResourceOk(Closeable c) {
c.close();
}
void closeResourceWrapperOk(c) {
closeResourceOk(c);
}
void closeResourceDirectOK() {
closeResourceOk(new FileInputStream("file.txt"));
}
void closeResourceTransitiveOk()
closeResourceOk(new FileInputStream("file.txt"));
}
void closeOne(Closeable c1, Closeable c2) {
c2.close();
}
void closeOnlyOneBad() {
closeOne(new FileInputStream("1.txt"), new FileInputStream("2.txt")); // warning
}
```
Hint: you might find the `FormalMap.t` stored in `proc_data.extras` useful for this. This module lets you go back and forth between the index of a formal and its name. This utility module is also used in the `RacerD` and `TaintAnalysis` modules.
## (5) Making it practical
(a) Real resource leaks frequently involve failing to close resources along exceptional control-flow paths. For simplicity, the initial version of the current analysis uses a filtered view of the CFG that skips exceptional edges (`ProcCfg.Normal`). To find more bugs, you might want to switch to using `ProcCfg.Exceptional` and make sure that your analysis gets the right answer on some realistic exception examples like:
```
void tryWithResourcesOk() {
// this is syntactic sugar that makes sure stream gets closed
try (FileInputStream stream = new FileInputStream("file.txt")) {
...
}
}
void closeInFinallyOk() {
FileInputStream stream = null;
try {
stream = new FileInputStream("file.txt");
} finally {
if (stream != null) {
stream.close();
}
}
}
void closeInCatchBad() {
FileInputStream stream = null;
try {
stream = new FileInputStream("file.txt");
} catch {
// ok not to close here, since we never acquired the resource
}
if (stream != null) {
stream.close();
}
}
```
(b) Try running on real code! The instructions [here](http://fm.csl.sri.com/SSFT17/infer-instr.html) have several suggestions for open-source Android apps to point your analysis at. Try `./gradlew assembleDebug -x test` first to make sure everything builds correctly without Infer (if not, you are probably missing some dependencies--the error messages should guide you). Once that's working, try
`./gradlew clean; infer run --resource-leak-only -- ./gradlew assembleDebug -x test`.
- Found a real bug? Bonus points! Send a pull request to fix it! Very frequently, the best fix is to use try-with-resources.
- Found a false positive in your analysis? Try re-running Infer with `--debug` and see if you can narrow down the root cause/fix it.
- How does your analysis compare to Infer's production resource leak analysis? Run with `infer -- <gradle command>` to see if your analysis finds bugs that Infer misses, or vice versa.

@ -39,4 +39,5 @@ public class Leaks {
stream1.close(); stream1.close();
stream2.close(); stream2.close();
} }
} }

@ -0,0 +1,22 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class LeaksAccessPaths {
void acquireTwoThenReleaseOneTwiceBad() throws IOException, FileNotFoundException {
FileInputStream stream1 = new FileInputStream("file1.txt");
FileInputStream stream2 = new FileInputStream("file2.txt");
stream1.close();
stream1.close();
}
}

@ -0,0 +1,40 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class LeaksAccessPathsInterprocedural {
void closeResourceOk(Closeable c) throws IOException {
c.close();
}
void closeResourceWrapperOk(Closeable c) throws IOException {
closeResourceOk(c);
}
void closeResourceDirectOK() throws IOException, FileNotFoundException {
closeResourceOk(new FileInputStream("file.txt"));
}
void closeResourceTransitiveOk() throws IOException, FileNotFoundException {
closeResourceOk(new FileInputStream("file.txt"));
}
void closeOne(Closeable c1, Closeable c2) throws IOException {
c2.close();
}
void closeOnlyOneBad() throws IOException, FileNotFoundException {
closeOne(new FileInputStream("1.txt"), new FileInputStream("2.txt")); // warning
}
}

@ -0,0 +1,23 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.FileInputStream;
import java.io.IOException;
public class LeaksAliasing {
void releaseBothOk(FileInputStream stream1, FileInputStream stream2) throws IOException {
if (stream1 == stream2) {
stream1.close();
} else {
stream1.close();
stream2.close();
}
}
}

@ -0,0 +1,31 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class LeaksBranch {
void mayLeakBad(Boolean b) throws IOException, FileNotFoundException {
FileInputStream stream;
if (b) {
stream = new FileInputStream("file.txt");
}
}
void choiceCloseOk(Boolean b) throws IOException, FileNotFoundException {
FileInputStream stream = new FileInputStream("file.txt");
if (b) {
stream.close();
} else {
stream.close();
}
}
}

@ -0,0 +1,64 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class LeaksExceptions {
void tryWithResourcesOk() throws IOException, FileNotFoundException {
// this is syntactic sugar that makes sure stream gets closed
try (FileInputStream stream = new FileInputStream("file.txt")) {
// do something with stream here
}
}
void closeInFinallyOk() throws IOException, FileNotFoundException {
FileInputStream stream = null;
try {
stream = new FileInputStream("file.txt");
} finally {
if (stream != null) {
stream.close();
}
}
}
void twoResourcesBad() throws IOException, FileNotFoundException {
FileInputStream stream1 = null;
FileInputStream stream2 = null;
try {
stream1 = new FileInputStream("file1.txt");
stream2 = new FileInputStream("file2.txt");
} finally {
if (stream1 != null) {
stream1.close(); // close() can throw!
}
if (stream2 != null) {
stream2.close(); // then this is never reached and stream2 leaks
}
}
}
void leakInCatchBad() throws IOException, FileNotFoundException {
FileInputStream stream = null;
try {
stream = new FileInputStream("file_in.txt");
} catch (Exception e) {
FileOutputStream fis = new FileOutputStream("file_out.txt");
// forgot to close fis
} finally {
if (stream != null) {
stream.close();
}
}
}
}

@ -0,0 +1,32 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class LeaksInterprocedural {
FileInputStream returnResourceOk() throws IOException, FileNotFoundException {
return new FileInputStream("file.txt");
}
FileInputStream returnResourceWrapperOk() throws IOException, FileNotFoundException {
return returnResourceOk();
}
void returnResourceThenCloseOk() throws IOException, FileNotFoundException {
returnResourceWrapperOk().close();
}
int returnResourceThenLeakBad() throws IOException, FileNotFoundException {
returnResourceWrapperOk(); // warning
return 0;
}
}

@ -0,0 +1,34 @@
/*
* Copyright (c) 2017-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.
*/
package codetoanalyze.java.checkers;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class LeaksLoop {
void openCloseLoopOk(String[] files) throws IOException, FileNotFoundException {
FileInputStream stream;
for (int i = 0; i < files.length; i++) {
String file = files[i];
stream = new FileInputStream(file);
stream.close();
}
}
void openAllCloseAllLoopOk(String[] files) throws IOException, FileNotFoundException {
FileInputStream[] streams = new FileInputStream[files.length];
for (int i = 0; i < files.length; i++) {
streams[i] = new FileInputStream(files[i]);
}
for (int i = 0; i < files.length; i++) {
streams[i].close();
}
}
}

@ -1,3 +1,10 @@
codetoanalyze/java/lab/Leaks.java, codetoanalyze.java.checkers.Leaks.acquireTwoForgetOneBad():void, 4, RESOURCE_LEAK, no_bucket, ERROR, [] codetoanalyze/java/lab/Leaks.java, codetoanalyze.java.checkers.Leaks.acquireTwoForgetOneBad():void, 4, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/Leaks.java, codetoanalyze.java.checkers.Leaks.basicLeakBad():void, 2, RESOURCE_LEAK, no_bucket, ERROR, [] codetoanalyze/java/lab/Leaks.java, codetoanalyze.java.checkers.Leaks.basicLeakBad():void, 2, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/Leaks.java, codetoanalyze.java.checkers.Leaks.doubleLeakBad():void, 3, RESOURCE_LEAK, no_bucket, ERROR, [] codetoanalyze/java/lab/Leaks.java, codetoanalyze.java.checkers.Leaks.doubleLeakBad():void, 3, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksAccessPathsInterprocedural.java, codetoanalyze.java.checkers.LeaksAccessPathsInterprocedural.closeOnlyOneBad():void, 2, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksBranch.java, codetoanalyze.java.checkers.LeaksBranch.mayLeakBad(java.lang.Boolean):void, 5, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksExceptions.java, codetoanalyze.java.checkers.LeaksExceptions.closeInFinallyOk():void, 9, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksExceptions.java, codetoanalyze.java.checkers.LeaksExceptions.leakInCatchBad():void, 12, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksExceptions.java, codetoanalyze.java.checkers.LeaksExceptions.tryWithResourcesOk():void, 5, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksExceptions.java, codetoanalyze.java.checkers.LeaksExceptions.twoResourcesBad():void, 14, RESOURCE_LEAK, no_bucket, ERROR, []
codetoanalyze/java/lab/LeaksInterprocedural.java, codetoanalyze.java.checkers.LeaksInterprocedural.returnResourceThenLeakBad():int, 2, RESOURCE_LEAK, no_bucket, ERROR, []

Loading…
Cancel
Save