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

(*
* 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