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