[pulse] Memory leak check

Summary:
First version of a new memory leak check based on Pulse. The idea is to examine unreachable cells in the heap and check that the "Allocated" attribute is available but the "Invalid CFree" isn't. This is done when we remove variables from the state.
Currently it only works for malloc, we can extend it to other allocation functions later.

Reviewed By: jvillard

Differential Revision: D20444097

fbshipit-source-id: 33b6b25a2
master
Dulma Churchill 5 years ago committed by Facebook GitHub Bot
parent 7ca2fcc948
commit e99295e0e9

@ -470,6 +470,7 @@ OPTIONS
PRECONDITION_NOT_FOUND (enabled by default), PRECONDITION_NOT_FOUND (enabled by default),
PRECONDITION_NOT_MET (enabled by default), PRECONDITION_NOT_MET (enabled by default),
PREMATURE_NIL_TERMINATION_ARGUMENT (enabled by default), PREMATURE_NIL_TERMINATION_ARGUMENT (enabled by default),
PULSE_MEMORY_LEAK (disabled by default),
PURE_FUNCTION (enabled by default), PURE_FUNCTION (enabled by default),
QUANDARY_TAINT_ERROR (enabled by default), QUANDARY_TAINT_ERROR (enabled by default),
REGISTERED_OBSERVER_BEING_DEALLOCATED (enabled by default), REGISTERED_OBSERVER_BEING_DEALLOCATED (enabled by default),

@ -201,6 +201,7 @@ OPTIONS
PRECONDITION_NOT_FOUND (enabled by default), PRECONDITION_NOT_FOUND (enabled by default),
PRECONDITION_NOT_MET (enabled by default), PRECONDITION_NOT_MET (enabled by default),
PREMATURE_NIL_TERMINATION_ARGUMENT (enabled by default), PREMATURE_NIL_TERMINATION_ARGUMENT (enabled by default),
PULSE_MEMORY_LEAK (disabled by default),
PURE_FUNCTION (enabled by default), PURE_FUNCTION (enabled by default),
QUANDARY_TAINT_ERROR (enabled by default), QUANDARY_TAINT_ERROR (enabled by default),
REGISTERED_OBSERVER_BEING_DEALLOCATED (enabled by default), REGISTERED_OBSERVER_BEING_DEALLOCATED (enabled by default),

@ -470,6 +470,7 @@ OPTIONS
PRECONDITION_NOT_FOUND (enabled by default), PRECONDITION_NOT_FOUND (enabled by default),
PRECONDITION_NOT_MET (enabled by default), PRECONDITION_NOT_MET (enabled by default),
PREMATURE_NIL_TERMINATION_ARGUMENT (enabled by default), PREMATURE_NIL_TERMINATION_ARGUMENT (enabled by default),
PULSE_MEMORY_LEAK (disabled by default),
PURE_FUNCTION (enabled by default), PURE_FUNCTION (enabled by default),
QUANDARY_TAINT_ERROR (enabled by default), QUANDARY_TAINT_ERROR (enabled by default),
REGISTERED_OBSERVER_BEING_DEALLOCATED (enabled by default), REGISTERED_OBSERVER_BEING_DEALLOCATED (enabled by default),

@ -406,6 +406,8 @@ let precondition_not_met = register_from_string "PRECONDITION_NOT_MET"
let premature_nil_termination = register_from_string "PREMATURE_NIL_TERMINATION_ARGUMENT" let premature_nil_termination = register_from_string "PREMATURE_NIL_TERMINATION_ARGUMENT"
let pulse_memory_leak = register_from_string ~enabled:false "PULSE_MEMORY_LEAK"
let pure_function = register_from_string "PURE_FUNCTION" let pure_function = register_from_string "PURE_FUNCTION"
let quandary_taint_error = register_from_string "QUANDARY_TAINT_ERROR" let quandary_taint_error = register_from_string "QUANDARY_TAINT_ERROR"

@ -264,6 +264,8 @@ val precondition_not_met : t
val premature_nil_termination : t val premature_nil_termination : t
val pulse_memory_leak : t
val pure_function : t val pure_function : t
val quandary_taint_error : t val quandary_taint_error : t

@ -167,7 +167,8 @@ module PulseTransferFunctions = struct
dispatch_call tenv summary ret call_exp actuals loc call_flags get_formals astate dispatch_call tenv summary ret call_exp actuals loc call_flags get_formals astate
|> check_error summary |> check_error summary
| Metadata (ExitScope (vars, location)) -> | Metadata (ExitScope (vars, location)) ->
[PulseOperations.remove_vars vars location astate] let astate = PulseOperations.remove_vars vars location astate in
[check_error summary astate]
| Metadata (VariableLifetimeBegins (pvar, _, location)) -> | Metadata (VariableLifetimeBegins (pvar, _, location)) ->
[PulseOperations.realloc_pvar pvar location astate] [PulseOperations.realloc_pvar pvar location astate]
| Metadata (Abstract _ | Nullify _ | Skip) -> | Metadata (Abstract _ | Nullify _ | Skip) ->

@ -27,6 +27,12 @@ module type BaseDomainSig = sig
val filter_addr : f:(AbstractValue.t -> bool) -> t -> t val filter_addr : f:(AbstractValue.t -> bool) -> t -> t
(**filter both heap and attrs *) (**filter both heap and attrs *)
val partition_addr :
f:(AbstractValue.t -> bool)
-> t
-> (BaseMemory.t * BaseAddressAttributes.t) * (BaseMemory.t * BaseAddressAttributes.t)
(**partition both heap and attrs *)
val pp : F.formatter -> t -> unit val pp : F.formatter -> t -> unit
end end
@ -54,6 +60,14 @@ module BaseDomainCommon = struct
let heap' = BaseMemory.filter (fun address _ -> f address) foot.heap in let heap' = BaseMemory.filter (fun address _ -> f address) foot.heap in
let attrs' = BaseAddressAttributes.filter (fun address _ -> f address) foot.attrs in let attrs' = BaseAddressAttributes.filter (fun address _ -> f address) foot.attrs in
update ~heap:heap' ~attrs:attrs' foot update ~heap:heap' ~attrs:attrs' foot
let partition_addr ~f foot =
let heap_yes, heap_no = BaseMemory.partition (fun address _ -> f address) foot.heap in
let attrs_yes, attrs_no =
BaseAddressAttributes.partition (fun address _ -> f address) foot.attrs
in
((heap_yes, attrs_yes), (heap_no, attrs_no))
end end
(** represents the post abstract state at each program point *) (** represents the post abstract state at each program point *)
@ -324,11 +338,15 @@ let discard_unreachable ({pre; post} as astate) =
in in
let post_addresses = BaseDomain.reachable_addresses (post :> BaseDomain.t) in let post_addresses = BaseDomain.reachable_addresses (post :> BaseDomain.t) in
let all_addresses = AbstractValue.Set.union pre_addresses post_addresses in let all_addresses = AbstractValue.Set.union pre_addresses post_addresses in
let post_new = let (heap_new, attrs_new), (_, attrs_unreachable) =
Domain.filter_addr ~f:(fun address -> AbstractValue.Set.mem address all_addresses) post Domain.partition_addr ~f:(fun address -> AbstractValue.Set.mem address all_addresses) post
in in
let post_new = Domain.update ~heap:heap_new ~attrs:attrs_new post in
let astate =
if phys_equal pre_new pre && phys_equal post_new post then astate if phys_equal pre_new pre && phys_equal post_new post then astate
else {astate with pre= pre_new; post= post_new} else {astate with pre= pre_new; post= post_new}
in
(astate, attrs_unreachable)
let is_local var astate = not (Var.is_return var || Stack.is_abducible astate var) let is_local var astate = not (Var.is_return var || Stack.is_abducible astate var)
@ -399,7 +417,9 @@ module PrePost = struct
let of_post pdesc astate = let of_post pdesc astate =
filter_for_summary astate |> discard_unreachable |> invalidate_locals pdesc let domain = filter_for_summary astate in
let domain, _ = discard_unreachable domain in
invalidate_locals pdesc domain
(* {2 machinery to apply a pre/post pair corresponding to a function's summary in a function call (* {2 machinery to apply a pre/post pair corresponding to a function's summary in a function call

@ -6,6 +6,7 @@
*) *)
open! IStd open! IStd
open PulseBasicInterface open PulseBasicInterface
module BaseAddressAttributes = PulseBaseAddressAttributes
module BaseDomain = PulseBaseDomain module BaseDomain = PulseBaseDomain
module BaseMemory = PulseBaseMemory module BaseMemory = PulseBaseMemory
module BaseStack = PulseBaseStack module BaseStack = PulseBaseStack
@ -103,9 +104,9 @@ end
module SkippedCalls : AbstractDomain.MapS with type key = Procname.t and type value = SkippedTrace.t module SkippedCalls : AbstractDomain.MapS with type key = Procname.t and type value = SkippedTrace.t
val discard_unreachable : t -> t val discard_unreachable : t -> t * BaseAddressAttributes.t
(** garbage collect unreachable addresses in the state to make it smaller, just for convenience and (** [discard_unreachable astate] garbage collects unreachable addresses in the state to make it
keep its size down *) smaller, and retuns the new state and the attributes of discarded addresses *)
val add_skipped_calls : Procname.t -> PulseTrace.t -> t -> t val add_skipped_calls : Procname.t -> PulseTrace.t -> t -> t

@ -45,6 +45,8 @@ let empty = Graph.empty
let filter = Graph.filter let filter = Graph.filter
let partition = Graph.partition
let pp = Graph.pp let pp = Graph.pp
let invalidate (address, history) invalidation location memory = let invalidate (address, history) invalidation location memory =

@ -14,6 +14,8 @@ val empty : t
val filter : (AbstractValue.t -> Attributes.t -> bool) -> t -> t val filter : (AbstractValue.t -> Attributes.t -> bool) -> t -> t
val partition : (AbstractValue.t -> Attributes.t -> bool) -> t -> t * t
val find_opt : AbstractValue.t -> t -> Attributes.t option val find_opt : AbstractValue.t -> t -> Attributes.t option
val add_one : AbstractValue.t -> Attribute.t -> t -> t val add_one : AbstractValue.t -> Attribute.t -> t -> t

@ -15,12 +15,13 @@ module ValueHistory = PulseValueHistory
type t = type t =
| AccessToInvalidAddress of | AccessToInvalidAddress of
{invalidation: Invalidation.t; invalidation_trace: Trace.t; access_trace: Trace.t} {invalidation: Invalidation.t; invalidation_trace: Trace.t; access_trace: Trace.t}
| MemoryLeak of {allocation_trace: Trace.t; location: Location.t}
| StackVariableAddressEscape of {variable: Var.t; history: ValueHistory.t; location: Location.t} | StackVariableAddressEscape of {variable: Var.t; history: ValueHistory.t; location: Location.t}
let get_location = function let get_location = function
| AccessToInvalidAddress {access_trace} -> | AccessToInvalidAddress {access_trace} ->
Trace.get_outer_location access_trace Trace.get_outer_location access_trace
| StackVariableAddressEscape {location} -> | MemoryLeak {location} | StackVariableAddressEscape {location} ->
location location
@ -66,6 +67,20 @@ let get_message = function
F.asprintf "%a%a" pp_access_trace access_trace F.asprintf "%a%a" pp_access_trace access_trace
(pp_invalidation_trace invalidation_line invalidation) (pp_invalidation_trace invalidation_line invalidation)
invalidation_trace invalidation_trace
| MemoryLeak {location; allocation_trace} ->
let allocation_line =
let {Location.line; _} = Trace.get_outer_location allocation_trace in
line
in
let pp_allocation_trace fmt (trace : Trace.t) =
match trace with
| Immediate _ ->
F.fprintf fmt "by call to `malloc()`"
| ViaCall {f; _} ->
F.fprintf fmt "by call to %a" CallEvent.describe f
in
F.asprintf "memory dynamically allocated at line %d %a, is not reachable after %a"
allocation_line pp_allocation_trace allocation_trace Location.pp location
| StackVariableAddressEscape {variable; _} -> | StackVariableAddressEscape {variable; _} ->
let pp_var f var = let pp_var f var =
if Var.is_cpp_temporary var then F.pp_print_string f "C++ temporary" if Var.is_cpp_temporary var then F.pp_print_string f "C++ temporary"
@ -95,6 +110,13 @@ let get_trace = function
~pp_immediate:(fun fmt -> F.pp_print_string fmt "invalid access occurs here") ~pp_immediate:(fun fmt -> F.pp_print_string fmt "invalid access occurs here")
access_trace access_trace
@@ [] @@ []
| MemoryLeak {allocation_trace} ->
let access_start_location = Trace.get_start_location allocation_trace in
add_errlog_header ~title:"allocation part of the trace starts here" access_start_location
@@ Trace.add_to_errlog ~nesting:0
~pp_immediate:(fun fmt -> F.pp_print_string fmt "allocation occurs here")
allocation_trace
@@ []
| StackVariableAddressEscape {history; location; _} -> | StackVariableAddressEscape {history; location; _} ->
ValueHistory.add_to_errlog ~nesting:0 history ValueHistory.add_to_errlog ~nesting:0 history
@@ @@
@ -105,5 +127,7 @@ let get_trace = function
let get_issue_type = function let get_issue_type = function
| AccessToInvalidAddress {invalidation; _} -> | AccessToInvalidAddress {invalidation; _} ->
Invalidation.issue_type_of_cause invalidation Invalidation.issue_type_of_cause invalidation
| MemoryLeak _ ->
IssueType.pulse_memory_leak
| StackVariableAddressEscape _ -> | StackVariableAddressEscape _ ->
IssueType.stack_variable_address_escape IssueType.stack_variable_address_escape

@ -14,6 +14,7 @@ module ValueHistory = PulseValueHistory
type t = type t =
| AccessToInvalidAddress of | AccessToInvalidAddress of
{invalidation: Invalidation.t; invalidation_trace: Trace.t; access_trace: Trace.t} {invalidation: Invalidation.t; invalidation_trace: Trace.t; access_trace: Trace.t}
| MemoryLeak of {allocation_trace: Trace.t; location: Location.t}
| StackVariableAddressEscape of {variable: Var.t; history: ValueHistory.t; location: Location.t} | StackVariableAddressEscape of {variable: Var.t; history: ValueHistory.t; location: Location.t}
val get_message : t -> string val get_message : t -> string

@ -468,6 +468,29 @@ let mark_address_of_stack_variable history variable location address astate =
AddressAttributes.add_one address (AddressOfStackVariable (variable, location, history)) astate AddressAttributes.add_one address (AddressOfStackVariable (variable, location, history)) astate
let check_memory_leak_unreachable unreachable_attrs location =
let check_memory_leak _ attributes result =
let allocated_not_freed_opt =
Attributes.fold attributes ~init:(None (* allocation trace *), false (* freed *))
~f:(fun acc attr ->
match (attr : Attribute.t) with
| Allocated trace ->
(Some trace, snd acc)
| Invalid (CFree, _) ->
(fst acc, true)
| _ ->
acc )
in
match allocated_not_freed_opt with
| Some trace, false ->
(* allocated but not freed *)
Error (Diagnostic.MemoryLeak {location; allocation_trace= trace})
| _ ->
result
in
PulseBaseAddressAttributes.fold check_memory_leak unreachable_attrs (Ok ())
let remove_vars vars location astate = let remove_vars vars location astate =
let astate = let astate =
List.fold vars ~init:astate ~f:(fun astate var -> List.fold vars ~init:astate ~f:(fun astate var ->
@ -485,7 +508,11 @@ let remove_vars vars location astate =
astate ) astate )
in in
let astate' = Stack.remove_vars vars astate in let astate' = Stack.remove_vars vars astate in
if phys_equal astate' astate then astate else AbductiveDomain.discard_unreachable astate' if phys_equal astate' astate then Ok astate
else
let astate, unreachable_attrs = AbductiveDomain.discard_unreachable astate' in
let+ () = check_memory_leak_unreachable unreachable_attrs location in
astate
let is_ptr_to_const formal_typ_opt = let is_ptr_to_const formal_typ_opt =

@ -99,7 +99,7 @@ val shallow_copy :
-> (t * (AbstractValue.t * ValueHistory.t)) access_result -> (t * (AbstractValue.t * ValueHistory.t)) access_result
(** returns the address of a new cell with the same edges as the original *) (** returns the address of a new cell with the same edges as the original *)
val remove_vars : Var.t list -> Location.t -> t -> t val remove_vars : Var.t list -> Location.t -> t -> t access_result
val check_address_escape : val check_address_escape :
Location.t -> Procdesc.t -> AbstractValue.t -> ValueHistory.t -> t -> t access_result Location.t -> Procdesc.t -> AbstractValue.t -> ValueHistory.t -> t -> t access_result

@ -0,0 +1,3 @@
codetoanalyze/c/pulse/memory_leak.c, malloc_interproc_no_free_bad, 0, PULSE_MEMORY_LEAK, no_bucket, ERROR, [allocation part of the trace starts here,when calling `create_p` here,allocation occurs here]
codetoanalyze/c/pulse/memory_leak.c, malloc_interproc_no_free_bad2, 4, PULSE_MEMORY_LEAK, no_bucket, ERROR, [allocation part of the trace starts here,allocation occurs here]
codetoanalyze/c/pulse/memory_leak.c, malloc_no_free_bad, 0, PULSE_MEMORY_LEAK, no_bucket, ERROR, [allocation part of the trace starts here,allocation occurs here]

@ -6,9 +6,7 @@
*/ */
#include <stdlib.h> #include <stdlib.h>
void malloc_no_free_bad() { // TODO implement the check void malloc_no_free_bad() { int* p = malloc(sizeof(p)); }
int* p = malloc(sizeof(p));
}
int* malloc_returned_ok() { int* malloc_returned_ok() {
int* p = malloc(sizeof(p)); int* p = malloc(sizeof(p));
@ -20,3 +18,17 @@ void malloc_then_free_ok() {
*p = 5; *p = 5;
free(p); free(p);
} }
int* create_p() {
int* p = malloc(sizeof(p));
return p;
}
void malloc_interproc_no_free_bad() { int* p = create_p(); }
void malloc_interproc_no_free_bad2() {
int* p = malloc(sizeof(p));
int z = 3;
int y = 4;
int* q = p;
}

Loading…
Cancel
Save