You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
12 KiB
303 lines
12 KiB
(*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* 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 CallEvent = PulseCallEvent
|
|
module Invalidation = PulseInvalidation
|
|
module Trace = PulseTrace
|
|
module ValueHistory = PulseValueHistory
|
|
|
|
type access_to_invalid_address =
|
|
{ calling_context: (CallEvent.t * Location.t) list
|
|
; invalidation: Invalidation.t
|
|
; invalidation_trace: Trace.t
|
|
; access_trace: Trace.t
|
|
; must_be_valid_reason: Invalidation.must_be_valid_reason option }
|
|
[@@deriving compare, equal]
|
|
|
|
type read_uninitialized_value = {calling_context: (CallEvent.t * Location.t) list; trace: Trace.t}
|
|
[@@deriving compare, equal]
|
|
|
|
let yojson_of_access_to_invalid_address = [%yojson_of: _]
|
|
|
|
let yojson_of_read_uninitialized_value = [%yojson_of: _]
|
|
|
|
type t =
|
|
| AccessToInvalidAddress of access_to_invalid_address
|
|
| MemoryLeak of {procname: Procname.t; allocation_trace: Trace.t; location: Location.t}
|
|
| ReadUninitializedValue of read_uninitialized_value
|
|
| StackVariableAddressEscape of {variable: Var.t; history: ValueHistory.t; location: Location.t}
|
|
[@@deriving equal]
|
|
|
|
let get_location = function
|
|
| AccessToInvalidAddress {calling_context= []; access_trace}
|
|
| ReadUninitializedValue {calling_context= []; trace= access_trace} ->
|
|
Trace.get_outer_location access_trace
|
|
| AccessToInvalidAddress {calling_context= (_, location) :: _}
|
|
| ReadUninitializedValue {calling_context= (_, location) :: _} ->
|
|
(* report at the call site that triggers the bug *) location
|
|
| MemoryLeak {location} | StackVariableAddressEscape {location} ->
|
|
location
|
|
|
|
|
|
let get_invalidation_in_trace trace =
|
|
PulseTrace.find_map trace ~f:(function
|
|
| ValueHistory.Invalidated (invalidation, _) ->
|
|
Some invalidation
|
|
| _ ->
|
|
None )
|
|
|
|
|
|
let trace_contains_invalidation trace =
|
|
PulseTrace.exists trace ~f:(function ValueHistory.Invalidated _ -> true | _ -> false)
|
|
|
|
|
|
(* whether the [calling_context + trace] starts with a call or contains only an immediate event *)
|
|
let immediate_or_first_call calling_context (trace : Trace.t) =
|
|
match (calling_context, trace) with
|
|
| [], Immediate _ ->
|
|
`Immediate
|
|
| (f, _) :: _, _ | [], ViaCall {f; _} ->
|
|
`Call f
|
|
|
|
|
|
let get_message diagnostic =
|
|
let pulse_start_msg = "Pulse found a potential" in
|
|
match diagnostic with
|
|
| AccessToInvalidAddress {calling_context; invalidation; invalidation_trace; access_trace} -> (
|
|
let invalidation, invalidation_trace =
|
|
get_invalidation_in_trace access_trace
|
|
|> Option.value_map
|
|
~f:(fun invalidation -> (invalidation, access_trace))
|
|
~default:(invalidation, invalidation_trace)
|
|
in
|
|
match invalidation with
|
|
| ConstantDereference i when IntLit.equal i IntLit.zero ->
|
|
(* Special error message for nullptr dereference *)
|
|
let pp_access_trace fmt (trace : Trace.t) =
|
|
match immediate_or_first_call calling_context trace with
|
|
| `Immediate ->
|
|
()
|
|
| `Call f ->
|
|
F.fprintf fmt " in call to %a" CallEvent.describe f
|
|
in
|
|
let pp_invalidation_trace line fmt (trace : Trace.t) =
|
|
let pp_line fmt line = F.fprintf fmt "on line %d" line in
|
|
match immediate_or_first_call calling_context trace with
|
|
| `Immediate ->
|
|
F.fprintf fmt "null pointer dereference %a" pp_line line
|
|
| `Call f ->
|
|
F.fprintf fmt "null pointer dereference %a indirectly during the call to %a" pp_line
|
|
line CallEvent.describe f
|
|
in
|
|
let invalidation_line =
|
|
let {Location.line; _} = Trace.get_outer_location invalidation_trace in
|
|
line
|
|
in
|
|
F.asprintf "%s %a%a." pulse_start_msg
|
|
(pp_invalidation_trace invalidation_line)
|
|
invalidation_trace pp_access_trace access_trace
|
|
| _ ->
|
|
(* The goal is to get one of the following messages depending on the scenario:
|
|
|
|
42: delete x; return x->f
|
|
"`x->f` accesses `x`, which was invalidated at line 42 by `delete` on `x`"
|
|
|
|
42: bar(x); return x->f
|
|
"`x->f` accesses `x`, which was invalidated at line 42 by `delete` on `x` in call to `bar`"
|
|
|
|
42: bar(x); foo(x);
|
|
"call to `foo` eventually accesses `x->f` but `x` was invalidated at line 42 by `delete` on `x` in call to `bar`"
|
|
|
|
If we don't have "x->f" but instead some non-user-visible expression, then
|
|
"access to `x`, which was invalidated at line 42 by `delete` on `x`"
|
|
|
|
Likewise if we don't have "x" in the second part but instead some non-user-visible expression, then
|
|
"`x->f` accesses `x`, which was invalidated at line 42 by `delete`"
|
|
*)
|
|
let pp_access_trace fmt (trace : Trace.t) =
|
|
match immediate_or_first_call calling_context trace with
|
|
| `Immediate ->
|
|
F.fprintf fmt "accessing memory that "
|
|
| `Call f ->
|
|
F.fprintf fmt "call to %a eventually accesses memory that " CallEvent.describe f
|
|
in
|
|
let pp_invalidation_trace line invalidation fmt (trace : Trace.t) =
|
|
let pp_line fmt line = F.fprintf fmt " on line %d" line in
|
|
match immediate_or_first_call calling_context trace with
|
|
| `Immediate ->
|
|
F.fprintf fmt "%a%a" Invalidation.describe invalidation pp_line line
|
|
| `Call f ->
|
|
F.fprintf fmt "%a%a indirectly during the call to %a" Invalidation.describe
|
|
invalidation pp_line line CallEvent.describe f
|
|
in
|
|
let invalidation_line =
|
|
let {Location.line; _} = Trace.get_outer_location invalidation_trace in
|
|
line
|
|
in
|
|
F.asprintf "%a%a" pp_access_trace access_trace
|
|
(pp_invalidation_trace invalidation_line invalidation)
|
|
invalidation_trace )
|
|
| MemoryLeak {procname; 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 `%a`" Procname.pp procname
|
|
| ViaCall {f; _} ->
|
|
F.fprintf fmt "by call to %a" CallEvent.describe f
|
|
in
|
|
F.asprintf
|
|
"%s memory leak. Memory dynamically allocated at line %d %a, is not freed after the last \
|
|
access at %a"
|
|
pulse_start_msg allocation_line pp_allocation_trace allocation_trace Location.pp location
|
|
| ReadUninitializedValue {calling_context; trace} ->
|
|
let root_var =
|
|
PulseTrace.find_map trace ~f:(function VariableDeclared (pvar, _) -> Some pvar | _ -> None)
|
|
|> IOption.if_none_evalopt ~f:(fun () ->
|
|
PulseTrace.find_map trace ~f:(function
|
|
| FormalDeclared (pvar, _) ->
|
|
Some pvar
|
|
| _ ->
|
|
None ) )
|
|
|> Option.map ~f:(F.asprintf "%a" Pvar.pp_value_non_verbose)
|
|
in
|
|
let declared_fields =
|
|
PulseTrace.find_map trace ~f:(function
|
|
| StructFieldAddressCreated (fields, _) ->
|
|
Some fields
|
|
| _ ->
|
|
None )
|
|
|> Option.map ~f:(F.asprintf "%a" ValueHistory.pp_fields)
|
|
in
|
|
let access_path =
|
|
match (root_var, declared_fields) with
|
|
| None, None ->
|
|
None
|
|
| Some root_var, None ->
|
|
Some root_var
|
|
| None, Some declared_fields ->
|
|
Some (F.sprintf "_.%s" declared_fields)
|
|
| Some root_var, Some declared_fields ->
|
|
Some (F.sprintf "%s.%s" root_var declared_fields)
|
|
in
|
|
let pp_access_path fmt = Option.iter access_path ~f:(F.fprintf fmt " `%s`") in
|
|
let pp_location fmt =
|
|
let {Location.line} = Trace.get_outer_location trace in
|
|
match immediate_or_first_call calling_context trace with
|
|
| `Immediate ->
|
|
F.fprintf fmt "on line %d" line
|
|
| `Call f ->
|
|
F.fprintf fmt "during the call to %a on line %d" CallEvent.describe f line
|
|
in
|
|
F.asprintf "%s uninitialized value%t being read on %t" pulse_start_msg pp_access_path
|
|
pp_location
|
|
| StackVariableAddressEscape {variable; _} ->
|
|
let pp_var f var =
|
|
if Var.is_cpp_temporary var then F.pp_print_string f "C++ temporary"
|
|
else F.fprintf f "stack variable `%a`" Var.pp var
|
|
in
|
|
F.asprintf "%s stack variable address escape. Address of %a is returned by the function"
|
|
pulse_start_msg pp_var variable
|
|
|
|
|
|
let add_errlog_header ~title location errlog =
|
|
let depth = 0 in
|
|
let tags = [] in
|
|
Errlog.make_trace_element depth location title tags :: errlog
|
|
|
|
|
|
let get_trace_calling_context calling_context errlog =
|
|
match calling_context with
|
|
| [] ->
|
|
errlog
|
|
| (_, first_call_loc) :: _ ->
|
|
add_errlog_header ~title:"calling context starts here" first_call_loc
|
|
@@ ( List.fold calling_context ~init:(errlog, 0) ~f:(fun (errlog, depth) (call, loc) ->
|
|
( Errlog.make_trace_element depth loc
|
|
(F.asprintf "in call to %a" CallEvent.pp call)
|
|
[]
|
|
:: errlog
|
|
, depth + 1 ) )
|
|
|> fst )
|
|
|
|
|
|
let add_invalidation_trace (invalidation : Invalidation.t) invalidation_trace access_trace errlog =
|
|
let start_title, access_title =
|
|
match invalidation with
|
|
| ConstantDereference i when IntLit.equal i IntLit.zero ->
|
|
( "source of the null value part of the trace starts here"
|
|
, "null pointer dereference part of the trace starts here" )
|
|
| ConstantDereference _ ->
|
|
( "source of the constant value part of the trace starts here"
|
|
, "constant value dereference part of the trace starts here" )
|
|
| CFree
|
|
| CppDelete
|
|
| EndIterator
|
|
| GoneOutOfScope _
|
|
| OptionalEmpty
|
|
| StdVector _
|
|
| JavaIterator _ ->
|
|
( "invalidation part of the trace starts here"
|
|
, "use-after-lifetime part of the trace starts here" )
|
|
in
|
|
let start_location = Trace.get_start_location invalidation_trace in
|
|
add_errlog_header ~title:start_title start_location
|
|
@@ Trace.add_to_errlog ~nesting:1
|
|
~pp_immediate:(fun fmt -> F.fprintf fmt "%a" Invalidation.describe invalidation)
|
|
invalidation_trace
|
|
@@
|
|
let access_start_location = Trace.get_start_location access_trace in
|
|
add_errlog_header ~title:access_title access_start_location @@ errlog
|
|
|
|
|
|
let get_trace = function
|
|
| AccessToInvalidAddress {calling_context; invalidation; invalidation_trace; access_trace} ->
|
|
get_trace_calling_context calling_context
|
|
@@ ( if trace_contains_invalidation access_trace then Fn.id
|
|
else add_invalidation_trace invalidation invalidation_trace access_trace )
|
|
@@ Trace.add_to_errlog ~nesting:1
|
|
~pp_immediate:(fun fmt -> F.pp_print_string fmt "invalid access occurs here")
|
|
access_trace
|
|
@@ []
|
|
| MemoryLeak {location; 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:1
|
|
~pp_immediate:(fun fmt -> F.pp_print_string fmt "allocation part of the trace ends here")
|
|
allocation_trace
|
|
@@ [Errlog.make_trace_element 0 location "memory becomes unreachable here" []]
|
|
| ReadUninitializedValue {calling_context; trace} ->
|
|
get_trace_calling_context calling_context
|
|
@@ Trace.add_to_errlog ~nesting:0
|
|
~pp_immediate:(fun fmt -> F.pp_print_string fmt "read to uninitialized value occurs here")
|
|
trace
|
|
@@ []
|
|
| StackVariableAddressEscape {history; location; _} ->
|
|
ValueHistory.add_to_errlog ~nesting:0 history
|
|
@@
|
|
let nesting = 0 in
|
|
[Errlog.make_trace_element nesting location "returned here" []]
|
|
|
|
|
|
let get_issue_type = function
|
|
| AccessToInvalidAddress {invalidation; must_be_valid_reason; access_trace} ->
|
|
let invalidation =
|
|
get_invalidation_in_trace access_trace |> Option.value ~default:invalidation
|
|
in
|
|
Invalidation.issue_type_of_cause invalidation must_be_valid_reason
|
|
| MemoryLeak _ ->
|
|
IssueType.pulse_memory_leak
|
|
| ReadUninitializedValue _ ->
|
|
IssueType.uninitialized_value_pulse
|
|
| StackVariableAddressEscape _ ->
|
|
IssueType.stack_variable_address_escape
|