Summary: A simple intraprocedural analysis that tracks when a storage location is read or deleted. For now, this works only with local variable storage locations; field and array accesses are ignored. In order to test this, I added a new "use-after-lifetime" warning. It complains when a variable is read or deleted after it has already been deleted. Reviewed By: jeremydubreil Differential Revision: D6961314 fbshipit-source-id: 75e95a2master
parent
e812934781
commit
d76d20a9ca
@ -0,0 +1,151 @@
|
|||||||
|
(*
|
||||||
|
* Copyright (c) 2018 - present Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*)
|
||||||
|
|
||||||
|
open! IStd
|
||||||
|
module F = Format
|
||||||
|
module L = Logging
|
||||||
|
|
||||||
|
type permission =
|
||||||
|
| TransferOwnership
|
||||||
|
(** permission to permanently transfer ownership (e.g. call a destructor, delete, or free) *)
|
||||||
|
| Read (** permission to read *)
|
||||||
|
|
||||||
|
module Permission = struct
|
||||||
|
type astate = permission
|
||||||
|
|
||||||
|
let ( <= ) ~lhs ~rhs =
|
||||||
|
match (lhs, rhs) with
|
||||||
|
| Read, TransferOwnership ->
|
||||||
|
true
|
||||||
|
| Read, Read | TransferOwnership, TransferOwnership ->
|
||||||
|
true
|
||||||
|
| TransferOwnership, Read ->
|
||||||
|
false
|
||||||
|
|
||||||
|
|
||||||
|
let join astate1 astate2 =
|
||||||
|
match (astate1, astate2) with
|
||||||
|
| Read, Read ->
|
||||||
|
Read
|
||||||
|
| TransferOwnership, _ | _, TransferOwnership ->
|
||||||
|
TransferOwnership
|
||||||
|
|
||||||
|
|
||||||
|
let widen ~prev ~next ~num_iters:_ = join prev next
|
||||||
|
|
||||||
|
let pp fmt = function
|
||||||
|
| TransferOwnership ->
|
||||||
|
F.fprintf fmt "TransferOwnership"
|
||||||
|
| Read ->
|
||||||
|
F.fprintf fmt "Read"
|
||||||
|
end
|
||||||
|
|
||||||
|
(** map from variable to permission required based on the way the variable is used. for example, if
|
||||||
|
we see delete x, then x needs permission "TransferOwnership" *)
|
||||||
|
module Domain = struct
|
||||||
|
include AbstractDomain.Map (Var) (Permission)
|
||||||
|
|
||||||
|
let log_use_after_lifetime var loc summary =
|
||||||
|
let message = F.asprintf "Variable %a is used after its lifetime has ended" Var.pp var in
|
||||||
|
let ltr = [Errlog.make_trace_element 0 loc "Use of invalid variable" []] in
|
||||||
|
let exn = Exceptions.Checkers (IssueType.use_after_lifetime, Localise.verbatim_desc message) in
|
||||||
|
Reporting.log_error summary ~loc ~ltr exn
|
||||||
|
|
||||||
|
|
||||||
|
(* don't allow strong updates via add; only remove *)
|
||||||
|
let add var new_permission loc summary astate =
|
||||||
|
let permission =
|
||||||
|
try
|
||||||
|
let old_permission = find var astate in
|
||||||
|
( match (old_permission, new_permission) with
|
||||||
|
| TransferOwnership, (Read | TransferOwnership) ->
|
||||||
|
log_use_after_lifetime var loc summary
|
||||||
|
| _ ->
|
||||||
|
() ) ;
|
||||||
|
Permission.join new_permission old_permission
|
||||||
|
with Not_found -> new_permission
|
||||||
|
in
|
||||||
|
add var permission astate
|
||||||
|
|
||||||
|
|
||||||
|
let access_path_add_read ((base_var, _), _) loc summary astate =
|
||||||
|
add base_var Read loc summary astate
|
||||||
|
|
||||||
|
|
||||||
|
let exp_add_reads exp loc summary astate =
|
||||||
|
List.fold
|
||||||
|
~f:(fun acc access_path -> access_path_add_read access_path loc summary acc)
|
||||||
|
(HilExp.get_access_paths exp) ~init:astate
|
||||||
|
|
||||||
|
|
||||||
|
let actuals_add_reads actuals loc summary astate =
|
||||||
|
(* TODO: handle reads in actuals + return values properly. This is nontrivial because the
|
||||||
|
frontend sometimes chooses to translate return as pass-by-ref on a dummy actual *)
|
||||||
|
List.fold actuals
|
||||||
|
~f:(fun acc actual_exp -> exp_add_reads actual_exp loc summary acc)
|
||||||
|
~init:astate
|
||||||
|
|
||||||
|
|
||||||
|
(* handle assigning directly to a base var *)
|
||||||
|
let handle_var_assign lhs_var rhs_exp loc summary astate =
|
||||||
|
exp_add_reads rhs_exp loc summary astate |> remove lhs_var
|
||||||
|
end
|
||||||
|
|
||||||
|
module TransferFunctions (CFG : ProcCfg.S) = struct
|
||||||
|
module CFG = CFG
|
||||||
|
module Domain = Domain
|
||||||
|
|
||||||
|
type extras = Specs.summary
|
||||||
|
|
||||||
|
let transfers_ownership pname =
|
||||||
|
(* TODO: support delete[], free, and destructors *)
|
||||||
|
Typ.Procname.equal pname BuiltinDecl.__delete
|
||||||
|
|
||||||
|
|
||||||
|
let exec_instr (astate: Domain.astate) (proc_data: extras ProcData.t) _ (instr: HilInstr.t) =
|
||||||
|
let summary = proc_data.extras in
|
||||||
|
match instr with
|
||||||
|
| Assign (Base (lhs_var, _), rhs_exp, loc) ->
|
||||||
|
Domain.handle_var_assign lhs_var rhs_exp loc summary astate
|
||||||
|
| Assign (lhs_access_exp, rhs_exp, loc) ->
|
||||||
|
(* assign to field, array, indirectly with &/*, or a combination *)
|
||||||
|
Domain.exp_add_reads rhs_exp loc summary astate
|
||||||
|
|> Domain.access_path_add_read (AccessExpression.to_access_path lhs_access_exp) loc summary
|
||||||
|
| Call (_, Direct callee_pname, [(AccessExpression Base (lhs_var, _))], _, loc)
|
||||||
|
when transfers_ownership callee_pname ->
|
||||||
|
Domain.add lhs_var TransferOwnership loc summary astate
|
||||||
|
| Call (_, Direct callee_pname, [(AccessExpression Base (lhs_var, _)); rhs_exp], _, loc)
|
||||||
|
when String.equal (Typ.Procname.get_method callee_pname) "operator=" ->
|
||||||
|
Domain.handle_var_assign lhs_var rhs_exp loc summary astate
|
||||||
|
| Call (None, Direct callee_pname, lhs_exp :: actuals, _, loc)
|
||||||
|
when Typ.Procname.is_constructor callee_pname ->
|
||||||
|
(* frontend translates constructors as assigning-by-ref to first actual *)
|
||||||
|
let constructed_var =
|
||||||
|
match lhs_exp with
|
||||||
|
| AccessExpression Base (var, _) ->
|
||||||
|
var
|
||||||
|
| _ ->
|
||||||
|
L.die InternalError "Unexpected: constructor called on %a" HilExp.pp lhs_exp
|
||||||
|
in
|
||||||
|
Domain.actuals_add_reads actuals loc summary astate |> Domain.remove constructed_var
|
||||||
|
| Call (ret_opt, _, actuals, _, loc)
|
||||||
|
-> (
|
||||||
|
let astate' = Domain.actuals_add_reads actuals loc summary astate in
|
||||||
|
match ret_opt with Some (base_var, _) -> Domain.remove base_var astate' | None -> astate' )
|
||||||
|
| Assume (assume_exp, _, _, loc) ->
|
||||||
|
Domain.exp_add_reads assume_exp loc summary astate
|
||||||
|
end
|
||||||
|
|
||||||
|
module Analyzer = LowerHil.MakeAbstractInterpreter (ProcCfg.Exceptional) (TransferFunctions)
|
||||||
|
|
||||||
|
let checker {Callbacks.proc_desc; tenv; summary} =
|
||||||
|
let proc_data = ProcData.make proc_desc tenv summary in
|
||||||
|
let initial = Domain.empty in
|
||||||
|
ignore (Analyzer.compute_post proc_data ~initial) ;
|
||||||
|
summary
|
@ -0,0 +1,12 @@
|
|||||||
|
(*
|
||||||
|
* Copyright (c) 2018 - 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
|
||||||
|
|
||||||
|
val checker : Callbacks.proc_callback_t
|
@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2018 - 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.
|
||||||
|
|
||||||
|
TESTS_DIR = ../../..
|
||||||
|
|
||||||
|
ANALYZER = checkers
|
||||||
|
# see explanations in cpp/errors/Makefile for the custom isystem
|
||||||
|
CLANG_OPTIONS = -x c++ -std=c++14 -Wc++1z-extensions -nostdinc++ -isystem$(MODELS_DIR)/cpp/include -isystem$(CLANG_INCLUDES)/c++/v1/ -c
|
||||||
|
INFER_OPTIONS = --ownership-only --ml-buckets cpp --debug-exceptions --project-root $(TESTS_DIR)
|
||||||
|
INFERPRINT_OPTIONS = --issues-tests
|
||||||
|
|
||||||
|
SOURCES = $(wildcard *.cpp)
|
||||||
|
|
||||||
|
include $(TESTS_DIR)/clang.make
|
||||||
|
|
||||||
|
infer-out/report.json: $(MAKEFILE_LIST)
|
@ -0,0 +1,8 @@
|
|||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, delete_in_branch_bad, 5, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, delete_in_loop_bad, 3, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, deref_deleted_bad, 3, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, double_delete_bad, 3, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, reassign_field_of_deleted_bad, 3, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, return_deleted_bad, 3, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, use_in_branch_bad, 4, USE_AFTER_LIFETIME, [Use of invalid variable]
|
||||||
|
codetoanalyze/cpp/ownership/use_after_delete.cpp, use_in_loop_bad, 4, USE_AFTER_LIFETIME, [Use of invalid variable]
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018 - 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct S {
|
||||||
|
int f;
|
||||||
|
};
|
||||||
|
|
||||||
|
void deref_deleted_bad() {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
S tmp = *s;
|
||||||
|
}
|
||||||
|
|
||||||
|
S* return_deleted_bad() {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
S* reassign_deleted_ok() {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
s = new S{2};
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reassign_field_of_deleted_bad() {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
s->f = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reassign_field_of_reinitialized_ok(S* tmp) {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
s = tmp;
|
||||||
|
s->f = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
void double_delete_bad() {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
delete s;
|
||||||
|
}
|
||||||
|
|
||||||
|
S* delete_in_branch_bad(bool b) {
|
||||||
|
auto s = new S{1};
|
||||||
|
if (b) {
|
||||||
|
delete s;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_in_branch_ok(bool b) {
|
||||||
|
auto s = new S{1};
|
||||||
|
if (b) {
|
||||||
|
delete s;
|
||||||
|
} else {
|
||||||
|
delete s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void use_in_branch_bad(bool b) {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
if (b) {
|
||||||
|
auto tmp = *s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_in_loop_bad() {
|
||||||
|
auto s = new S{1};
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
delete s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_in_loop_ok() {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void use_in_loop_bad() {
|
||||||
|
auto s = new S{1};
|
||||||
|
delete s;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
s->f = i;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue