Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memory Out-Of-Bounds Access Analysis #1094

Merged
merged 41 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7bb5697
Add first rough (semi-working) memory OOB analysis
mrstanb Jun 27, 2023
a5c693e
Add 2 simple regression tests for memory OOB
mrstanb Jun 27, 2023
f350745
Stick with Lattice.Unit
mrstanb Jul 1, 2023
4d5e0dc
Rewrite the majority of the logic and checks performed in memOutOfBounds
mrstanb Jul 8, 2023
098501b
Remove printf calls
mrstanb Jul 8, 2023
6496c61
Improve warning messages a bit
mrstanb Jul 8, 2023
a796a0b
Add message category for memory OOB access
mrstanb Jul 8, 2023
84e80b1
Add CWE number 823 for memory OOB access warnings
mrstanb Jul 8, 2023
a07b690
Add a global variable that indicates whether an invalid pointer deref…
mrstanb Jul 8, 2023
4c09372
Add OOB memory access regression case with a loop
mrstanb Jul 26, 2023
f90770f
Merge branch 'master' into mem-oob-analysis
mrstanb Aug 12, 2023
503fb24
Remove wrongly overtaken goblintutil.ml
mrstanb Aug 12, 2023
0c53230
Move may_invalid_deref to analysisState
mrstanb Aug 12, 2023
c07d867
Use AnalysisState in place of Goblintutil
mrstanb Aug 12, 2023
434a4c1
Enable intervals for regression tests
mrstanb Aug 20, 2023
3db3d8c
Fix memOutOfBounds analysis and make it work
mrstanb Aug 20, 2023
6e7664d
Remove unused transfer funs
mrstanb Aug 20, 2023
50777a2
Merge branch 'master' into mem-oob-analysis
mrstanb Aug 20, 2023
79d4319
Move regression tests to a correctly numbered folder
mrstanb Aug 20, 2023
84bd2cc
Add comments to 77/03 test case
mrstanb Aug 20, 2023
6ed476e
Warn for function args in enter and not in combine_assign
mrstanb Aug 21, 2023
4e7b21b
Try to check and warn only upon dereferences
mrstanb Aug 24, 2023
86b7a54
Merge branch 'master' into mem-oob-analysis
mrstanb Aug 24, 2023
291d60c
Check for OOB mem access on the address level and only warn on deref
mrstanb Aug 25, 2023
2a3aaae
Adjust the OOB mem access test case with a loop
mrstanb Aug 25, 2023
46bd81f
Add test case with pointer arithmetic and subsequent derefs
mrstanb Aug 25, 2023
37fb4e8
Refactor to_int_dom_offs
mrstanb Aug 25, 2023
ff3e644
Cover bot and top cases in to_int_dom_offs
mrstanb Aug 25, 2023
aceffa8
Allow Queries.BlobSize to be asked for the size from the start address
mrstanb Aug 31, 2023
7fb6719
Use Queries.BlobSize for getting blob sizes without address offsets
mrstanb Aug 31, 2023
0e126df
Clean up unnecessary comments
mrstanb Aug 31, 2023
293d3e7
Add check for implicit pointer dereferences in special functions
mrstanb Aug 31, 2023
e4b349a
Add a test case with implicit dereferences
mrstanb Aug 31, 2023
4746eac
Merge branch 'master' into mem-oob-analysis
michael-schwarz Sep 6, 2023
2b45499
Disable Info warnings for test case 77/05 for now
mrstanb Sep 6, 2023
9241301
Remove TODO comment and add a few other explaining comments
mrstanb Sep 6, 2023
0bae455
Use a record instead of a tuple for Queries.BlobSize
mrstanb Sep 6, 2023
f06a0f5
Add check for bot and top address offsets
mrstanb Sep 26, 2023
44a3f4d
Merge branch 'master' into mem-oob-analysis
mrstanb Sep 26, 2023
a2bea7b
Fix issues after merge with master
mrstanb Sep 27, 2023
d48f232
Merge branch 'master' into mem-oob-analysis
michael-schwarz Sep 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/analyses/base.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1257,16 +1257,26 @@ struct
end
| Q.EvalValue e ->
eval_rv (Analyses.ask_of_ctx ctx) ctx.global ctx.local e
| Q.BlobSize e -> begin
| Q.BlobSize (e, from_base_addr) -> begin
let p = eval_rv_address (Analyses.ask_of_ctx ctx) ctx.global ctx.local e in
(* ignore @@ printf "BlobSize %a MayPointTo %a\n" d_plainexp e VD.pretty p; *)
match p with
| Address a ->
let s = addrToLvalSet a in
(* If there's a non-heap var or an offset in the lval set, we answer with bottom *)
if ValueDomainQueries.LS.exists (fun (v, o) -> (not @@ ctx.ask (Queries.IsHeapVar v)) || o <> `NoOffset) s then
(* If we're asking for the BlobSize from the base address, then don't check for offsets => we want to avoid getting bot *)
if ValueDomainQueries.LS.exists (fun (v, o) -> (not @@ ctx.ask (Queries.IsHeapVar v)) || (if not from_base_addr then o <> `NoOffset else false)) s then
Queries.Result.bot q
else (
(* If we need the BlobSize from the base address, then remove any offsets *)
let a =
if from_base_addr then
ValueDomainQueries.LS.elements s
|> List.map (fun (v, _) -> Addr.of_var v)
|> AD.of_list
else
a
in
let r = get ~full:true (Analyses.ask_of_ctx ctx) ctx.global ctx.local a None in
(* ignore @@ printf "BlobSize %a = %a\n" d_plainexp e VD.pretty r; *)
(match r with
Expand Down
344 changes: 344 additions & 0 deletions src/analyses/memOutOfBounds.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
open GoblintCil
open Analyses
open MessageCategory

module AS = AnalysisState
module VDQ = ValueDomainQueries

module Spec =
mrstanb marked this conversation as resolved.
Show resolved Hide resolved
struct
include Analyses.IdentitySpec

module D = Lattice.Unit
module C = D

(* TODO: Check out later for benchmarking *)
let context _ _ = ()
mrstanb marked this conversation as resolved.
Show resolved Hide resolved

let name () = "memOutOfBounds"

(* HELPER FUNCTIONS *)

let intdom_of_int x =
IntDomain.IntDomTuple.of_int (Cilfacade.ptrdiff_ikind ()) (Z.of_int x)

let to_index ?typ offs =
let idx_of_int x =
IntDomain.IntDomTuple.of_int (Cilfacade.ptrdiff_ikind ()) (Z.of_int (x / 8))
in
let rec offset_to_index_offset ?typ offs = match offs with
| `NoOffset -> idx_of_int 0
| `Field (field, o) ->
let field_as_offset = Field (field, NoOffset) in
let bits_offset, _size = GoblintCil.bitsOffset (TComp (field.fcomp, [])) field_as_offset in
let bits_offset = idx_of_int bits_offset in
let remaining_offset = offset_to_index_offset ~typ:field.ftype o in
IntDomain.IntDomTuple.add bits_offset remaining_offset
| `Index (x, o) ->
let (item_typ, item_size_in_bits) =
match Option.map unrollType typ with
| Some TArray(item_typ, _, _) ->
let item_size_in_bits = bitsSizeOf item_typ in
(Some item_typ, idx_of_int item_size_in_bits)
| _ ->
(None, IntDomain.IntDomTuple.top_of @@ Cilfacade.ptrdiff_ikind ())
in
let bits_offset = IntDomain.IntDomTuple.mul item_size_in_bits x in
let remaining_offset = offset_to_index_offset ?typ:item_typ o in
IntDomain.IntDomTuple.add bits_offset remaining_offset
in
offset_to_index_offset ?typ offs

let rec exp_contains_a_ptr (exp:exp) =
match exp with
| Const _
| SizeOf _
| SizeOfStr _
| AlignOf _
| AddrOfLabel _ -> false
| Real e
| Imag e
| SizeOfE e
| AlignOfE e
| UnOp (_, e, _)
| CastE (_, e) -> exp_contains_a_ptr e
| BinOp (_, e1, e2, _) ->
exp_contains_a_ptr e1 || exp_contains_a_ptr e2
| Question (e1, e2, e3, _) ->
exp_contains_a_ptr e1 || exp_contains_a_ptr e2 || exp_contains_a_ptr e3
| Lval lval
| AddrOf lval
| StartOf lval -> lval_contains_a_ptr lval

and lval_contains_a_ptr (lval:lval) =
let (host, offset) = lval in
let host_contains_a_ptr = function
| Var v -> isPointerType v.vtype
| Mem e -> exp_contains_a_ptr e
in
let rec offset_contains_a_ptr = function
| NoOffset -> false
| Index (e, o) -> exp_contains_a_ptr e || offset_contains_a_ptr o
| Field (f, o) -> isPointerType f.ftype || offset_contains_a_ptr o
in
host_contains_a_ptr host || offset_contains_a_ptr offset

let points_to_heap_only ctx ptr =
match ctx.ask (Queries.MayPointTo ptr) with
| a when not (Queries.LS.is_top a) && not (Queries.LS.mem (dummyFunDec.svar, `NoOffset) a) ->
Queries.LS.for_all (fun (v, _) -> ctx.ask (Queries.IsHeapVar v)) a
| _ -> false

let get_size_of_ptr_target ctx ptr =
if points_to_heap_only ctx ptr then
(* Ask for BlobSize from the base address (the second component being set to true) in order to avoid BlobSize giving us bot *)
ctx.ask (Queries.BlobSize (ptr, true))
else
match ctx.ask (Queries.MayPointTo ptr) with
| a when not (Queries.LS.is_top a) ->
let pts_list = Queries.LS.elements a in
let pts_elems_to_sizes (v, _) =
begin match v.vtype with
| TArray (item_typ, _, _) ->
let item_typ_size_in_bytes = (bitsSizeOf item_typ) / 8 in
let item_typ_size_in_bytes = intdom_of_int item_typ_size_in_bytes in
begin match ctx.ask (Queries.EvalLength ptr) with
| `Lifted arr_len -> `Lifted (IntDomain.IntDomTuple.mul item_typ_size_in_bytes arr_len)
| `Bot -> VDQ.ID.bot ()
| `Top -> VDQ.ID.top ()
end
| _ ->
let type_size_in_bytes = (bitsSizeOf v.vtype) / 8 in
`Lifted (intdom_of_int type_size_in_bytes)
end
in
(* Map each points-to-set element to its size *)
let pts_sizes = List.map pts_elems_to_sizes pts_list in
(* Take the smallest of all sizes that ptr's contents may have *)
begin match pts_sizes with
| [] -> VDQ.ID.bot ()
| [x] -> x
| x::xs -> List.fold_left (fun acc elem ->
if VDQ.ID.compare acc elem >= 0 then elem else acc
) x xs
end
| _ ->
M.warn "Pointer %a has a points-to-set of top. An invalid memory access might occur" d_exp ptr;
VDQ.ID.top ()

let get_ptr_deref_type ptr_typ =
match ptr_typ with
| TPtr (t, _) -> Some t
| _ -> None

let size_of_type_in_bytes typ =
let typ_size_in_bytes = (bitsSizeOf typ) / 8 in
intdom_of_int typ_size_in_bytes

let eval_ptr_offset_in_binop ctx exp ptr_contents_typ =
let eval_offset = ctx.ask (Queries.EvalInt exp) in
let eval_offset = Option.get @@ VDQ.ID.to_int eval_offset in
let eval_offset = VDQ.ID.of_int (Cilfacade.ptrdiff_ikind ()) eval_offset in
let ptr_contents_typ_size_in_bytes = size_of_type_in_bytes ptr_contents_typ in
match eval_offset with
| `Lifted i -> `Lifted (IntDomain.IntDomTuple.mul i ptr_contents_typ_size_in_bytes)
| `Top -> `Top
| `Bot -> `Bot

let rec offs_to_idx typ offs =
match offs with
| `NoOffset -> intdom_of_int 0
| `Field (field, o) ->
let field_as_offset = Field (field, NoOffset) in
let bits_offset, _size = GoblintCil.bitsOffset (TComp (field.fcomp, [])) field_as_offset in
let bytes_offset = intdom_of_int (bits_offset / 8) in
let remaining_offset = offs_to_idx field.ftype o in
IntDomain.IntDomTuple.add bytes_offset remaining_offset
| `Index (x, o) ->
let typ_size_in_bytes = size_of_type_in_bytes typ in
let bytes_offset = IntDomain.IntDomTuple.mul typ_size_in_bytes x in
let remaining_offset = offs_to_idx typ o in
IntDomain.IntDomTuple.add bytes_offset remaining_offset

let rec get_addr_offs ctx ptr =
match ctx.ask (Queries.MayPointTo ptr) with
| a when not (VDQ.LS.is_top a) ->
let ptr_deref_type = get_ptr_deref_type @@ typeOf ptr in
begin match ptr_deref_type with
| Some t ->
begin match VDQ.LS.is_empty a with
| true ->
M.warn "Pointer %a has an empty points-to-set" d_exp ptr;
IntDomain.IntDomTuple.top_of @@ Cilfacade.ptrdiff_ikind ()
| false ->
(*
Offset should be the same for all elements in the points-to set.
Hence, we can just pick one element and obtain its offset
*)
let (_, o) = VDQ.LS.choose a in
let rec to_int_dom_offs = function
| `NoOffset -> `NoOffset
| `Field (f, o) -> `Field (f, to_int_dom_offs o)
| `Index (i, o) ->
let exp_as_int_dom = match ctx.ask (Queries.EvalInt i) with
| `Lifted i -> i
| `Bot -> IntDomain.IntDomTuple.bot_of @@ Cilfacade.ptrdiff_ikind ()
| `Top -> IntDomain.IntDomTuple.top_of @@ Cilfacade.ptrdiff_ikind ()
in
`Index (exp_as_int_dom, to_int_dom_offs o)
in
offs_to_idx t (to_int_dom_offs o)
end
| None ->
M.error "Expression %a doesn't have pointer type" d_exp ptr;
IntDomain.IntDomTuple.top_of @@ Cilfacade.ptrdiff_ikind ()
end
| _ ->
M.warn "Pointer %a has a points-to-set of top. An invalid memory access might occur" d_exp ptr;
IntDomain.IntDomTuple.top_of @@ Cilfacade.ptrdiff_ikind ()

and check_lval_for_oob_access ctx ?(is_implicitly_derefed = false) lval =
if not @@ lval_contains_a_ptr lval then ()
else
(* If the lval doesn't indicate an explicit dereference, we still need to check for an implicit dereference *)
(* An implicit dereference is, e.g., printf("%p", ptr), where ptr is a pointer *)
match lval, is_implicitly_derefed with
| (Var _, _), false -> ()
| (Var v, _), true -> check_no_binop_deref ctx (Lval lval)
| (Mem e, _), _ ->
begin match e with
| Lval (Var v, _) as lval_exp -> check_no_binop_deref ctx lval_exp
| BinOp (binop, e1, e2, t) when binop = PlusPI || binop = MinusPI || binop = IndexPI ->
check_binop_exp ctx binop e1 e2 t;
check_exp_for_oob_access ctx ~is_implicitly_derefed e1;
check_exp_for_oob_access ctx ~is_implicitly_derefed e2
| _ -> check_exp_for_oob_access ctx ~is_implicitly_derefed e
end

and check_no_binop_deref ctx lval_exp =
let behavior = Undefined MemoryOutOfBoundsAccess in
let cwe_number = 823 in
let ptr_size = get_size_of_ptr_target ctx lval_exp in
let addr_offs = get_addr_offs ctx lval_exp in
let ptr_type = typeOf lval_exp in
let ptr_contents_type = get_ptr_deref_type ptr_type in
match ptr_contents_type with
| Some t ->
begin match VDQ.ID.is_top ptr_size with
| true ->
AS.svcomp_may_invalid_deref := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Size of pointer %a not known. Memory out-of-bounds access might occur due to pointer arithmetic" d_exp lval_exp
| false ->
let offs = `Lifted addr_offs in
if ptr_size < offs then begin
AS.svcomp_may_invalid_deref := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Size of pointer is %a (in bytes). It is offset by %a (in bytes) due to pointer arithmetic. Memory out-of-bounds access must occur" VDQ.ID.pretty ptr_size VDQ.ID.pretty offs
end
end
| _ -> M.error "Expression %a is not a pointer" d_exp lval_exp

and check_exp_for_oob_access ctx ?(is_implicitly_derefed = false) exp =
match exp with
| Const _
| SizeOf _
| SizeOfStr _
| AlignOf _
| AddrOfLabel _ -> ()
| Real e
| Imag e
| SizeOfE e
| AlignOfE e
| UnOp (_, e, _)
| CastE (_, e) -> check_exp_for_oob_access ctx ~is_implicitly_derefed e
| BinOp (bop, e1, e2, t) ->
check_exp_for_oob_access ctx ~is_implicitly_derefed e1;
check_exp_for_oob_access ctx ~is_implicitly_derefed e2
| Question (e1, e2, e3, _) ->
check_exp_for_oob_access ctx ~is_implicitly_derefed e1;
check_exp_for_oob_access ctx ~is_implicitly_derefed e2;
check_exp_for_oob_access ctx ~is_implicitly_derefed e3
| Lval lval
| StartOf lval
| AddrOf lval -> check_lval_for_oob_access ctx ~is_implicitly_derefed lval

and check_binop_exp ctx binop e1 e2 t =
let binopexp = BinOp (binop, e1, e2, t) in
let behavior = Undefined MemoryOutOfBoundsAccess in
let cwe_number = 823 in
match binop with
| PlusPI
| IndexPI
| MinusPI ->
let ptr_size = get_size_of_ptr_target ctx e1 in
let addr_offs = get_addr_offs ctx e1 in
let ptr_type = typeOf e1 in
let ptr_contents_type = get_ptr_deref_type ptr_type in
begin match ptr_contents_type with
| Some t ->
let offset_size = eval_ptr_offset_in_binop ctx e2 t in
(* Make sure to add the address offset to the binop offset *)
let offset_size_with_addr_size = match offset_size with
| `Lifted os -> `Lifted (IntDomain.IntDomTuple.add os addr_offs)
| `Top -> `Top
| `Bot -> `Bot
in
begin match VDQ.ID.is_top ptr_size, VDQ.ID.is_top offset_size_with_addr_size with
| true, _ ->
AS.svcomp_may_invalid_deref := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Size of pointer %a in expression %a not known. Memory out-of-bounds access might occur" d_exp e1 d_exp binopexp
| _, true ->
AS.svcomp_may_invalid_deref := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Operand value for pointer arithmetic in expression %a not known. Memory out-of-bounds access might occur" d_exp binopexp
| false, false ->
if ptr_size < offset_size_with_addr_size then begin
AS.svcomp_may_invalid_deref := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Size of pointer in expression %a is %a (in bytes). It is offset by %a (in bytes). Memory out-of-bounds access must occur" d_exp binopexp VDQ.ID.pretty ptr_size VDQ.ID.pretty offset_size_with_addr_size
end
end
| _ -> M.error "Binary expression %a doesn't have a pointer" d_exp binopexp
end
| _ -> ()


(* TRANSFER FUNCTIONS *)

let assign ctx (lval:lval) (rval:exp) : D.t =
check_lval_for_oob_access ctx lval;
check_exp_for_oob_access ctx rval;
ctx.local

let branch ctx (exp:exp) (tv:bool) : D.t =
check_exp_for_oob_access ctx exp;
ctx.local

let return ctx (exp:exp option) (f:fundec) : D.t =
Option.iter (fun x -> check_exp_for_oob_access ctx x) exp;
ctx.local

let special ctx (lval:lval option) (f:varinfo) (arglist:exp list) : D.t =
let desc = LibraryFunctions.find f in
let is_arg_implicitly_derefed arg =
let read_shallow_args = LibraryDesc.Accesses.find desc.accs { kind = Read; deep = false } arglist in
let read_deep_args = LibraryDesc.Accesses.find desc.accs { kind = Read; deep = true } arglist in
let write_shallow_args = LibraryDesc.Accesses.find desc.accs { kind = Write; deep = false } arglist in
let write_deep_args = LibraryDesc.Accesses.find desc.accs { kind = Write; deep = true } arglist in
List.mem arg read_shallow_args || List.mem arg read_deep_args || List.mem arg write_shallow_args || List.mem arg write_deep_args
in
Option.iter (fun x -> check_lval_for_oob_access ctx x) lval;
List.iter (fun arg -> check_exp_for_oob_access ctx ~is_implicitly_derefed:(is_arg_implicitly_derefed arg) arg) arglist;
ctx.local

let enter ctx (lval: lval option) (f:fundec) (args:exp list) : (D.t * D.t) list =
List.iter (fun arg -> check_exp_for_oob_access ctx arg) args;
[ctx.local, ctx.local]

let combine_assign ctx (lval:lval option) fexp (f:fundec) (args:exp list) fc (callee_local:D.t) (f_ask:Queries.ask) : D.t =
Option.iter (fun x -> check_lval_for_oob_access ctx x) lval;
ctx.local

let startstate v = ()
let exitstate v = ()
end

let _ =
MCP.register_analysis (module Spec : MCPSpec)
Loading