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

Improve multi-threaded UAF analysis and add SVComp result generation #1123

Merged
merged 24 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a57112b
Add regression test case with a joined thread
mrstanb Jul 26, 2023
0b3bfc9
Add global variable for detection of UAF occurrences
mrstanb Jul 26, 2023
77f7e89
Add SVComp result generation for memory safety
mrstanb Jul 26, 2023
cfef9bd
Fix multi-line if expression
mrstanb Jul 30, 2023
cbaffbb
Side-effect a tuple of tids and sets of joined threads
mrstanb Jul 30, 2023
c01585d
Warn if threadJoins is deactivated
mrstanb Jul 31, 2023
49a6d54
Update joined thread test case annotations
mrstanb Jul 31, 2023
b02d0a7
Remove threadJoins activation warning
mrstanb Aug 2, 2023
50f20b0
Use a map domain for the globals
mrstanb Aug 2, 2023
c90fb0f
Add global vars for all three memory safety SVComp properties
mrstanb Aug 2, 2023
e28f72b
Improve SVComp result generation and support all 3 memory-safety props
mrstanb Aug 2, 2023
a054a4f
Warn for UAF only in case there's a deref happening
mrstanb Aug 26, 2023
3271430
Adapt regression tests to not WARN for UAF if there's no deref
mrstanb Aug 26, 2023
d49db71
Clean up and use proper global vars for SV-COMP
mrstanb Aug 26, 2023
034b0ab
Use proper bug names when warning for multi-threaded programs
mrstanb Aug 27, 2023
68e1b71
Add a valid-memsafety.prp file
mrstanb Aug 27, 2023
cd95e8d
Try to not warn for threads joined before free()
mrstanb Aug 28, 2023
ba4d301
Enable threadJoins for all multi-threaded test cases
mrstanb Aug 28, 2023
66f0c9d
Add checks for implicit dereferencing of pointers
mrstanb Aug 28, 2023
5464636
Accommodate implicit dereferencing in regression tests
mrstanb Aug 28, 2023
049dc58
Remove redundant intersection when side-effecting
mrstanb Aug 28, 2023
7adff2f
Merge branch 'master' into improve-uaf-analysis
mrstanb Sep 26, 2023
75f1580
Improve double free check
mrstanb Sep 28, 2023
cb29ecd
Merge branch 'master' into improve-uaf-analysis
mrstanb Sep 29, 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
61 changes: 44 additions & 17 deletions src/analyses/useAfterFree.ml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
(** An analysis for the detection of use-after-free vulnerabilities ([useAfterFree]). *)

open GobConfig
open GoblintCil
open Analyses
open MessageCategory

module ToppedVarInfoSet = SetDomain.ToppedSet(CilType.Varinfo)(struct let topname = "All Heap Variables" end)
module ThreadIdSet = SetDomain.Make(ThreadIdDomain.ThreadLifted)
module ThreadIdToJoinedThreadsMap = MapDomain.MapBot(ThreadIdDomain.ThreadLifted)(ConcDomain.MustThreadSet)

module Spec : Analyses.MCPSpec =
struct
Expand All @@ -15,7 +16,7 @@ struct

module D = ToppedVarInfoSet
module C = Lattice.Unit
module G = ThreadIdSet
module G = ThreadIdToJoinedThreadsMap
module V = VarinfoV

(** TODO: Try out later in benchmarks to see how we perform with and without context-sensititivty *)
Expand All @@ -27,40 +28,54 @@ struct
let get_current_threadid ctx =
ctx.ask Queries.CurrentThreadId

let get_joined_threads ctx =
ctx.ask Queries.MustJoinedThreads

let warn_for_multi_threaded_access ctx (heap_var:varinfo) behavior cwe_number =
let freeing_threads = ctx.global heap_var in
(* If we're single-threaded or there are no threads freeing the memory, we have nothing to WARN about *)
if ctx.ask (Queries.MustBeSingleThreaded { since_start = true }) || ThreadIdSet.is_empty freeing_threads then ()
if ctx.ask (Queries.MustBeSingleThreaded { since_start = true }) || G.is_empty freeing_threads then ()
else begin
let possibly_started current = function
let possibly_started current tid joined_threads =
match tid with
| `Lifted tid ->
let threads = ctx.ask Queries.CreatedThreads in
let created_threads = ctx.ask Queries.CreatedThreads in
(* Discard joined threads, as they're supposed to be joined before the point of freeing the memory *)
let threads = ConcDomain.MustThreadSet.diff created_threads joined_threads in
let not_started = MHP.definitely_not_started (current, threads) tid in
let possibly_started = not not_started in
possibly_started
| `Top -> true
| `Bot -> false
in
let equal_current current = function
let equal_current current tid joined_threads =
match tid with
| `Lifted tid ->
ThreadId.Thread.equal current tid
| `Top -> true
| `Bot -> false
in
match get_current_threadid ctx with
| `Lifted current ->
let possibly_started = ThreadIdSet.exists (possibly_started current) freeing_threads in
if possibly_started then
let possibly_started = G.exists (possibly_started current) freeing_threads in
if possibly_started then begin
AnalysisState.svcomp_may_use_after_free := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "There's a thread that's been started in parallel with the memory-freeing threads for heap variable %a. Use-After-Free might occur" CilType.Varinfo.pretty heap_var
end
else begin
let current_is_unique = ThreadId.Thread.is_unique current in
let any_equal_current threads = ThreadIdSet.exists (equal_current current) threads in
if not current_is_unique && any_equal_current freeing_threads then
let any_equal_current threads = G.exists (equal_current current) threads in
if not current_is_unique && any_equal_current freeing_threads then begin
AnalysisState.svcomp_may_use_after_free := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Current thread is not unique and a Use-After-Free might occur for heap variable %a" CilType.Varinfo.pretty heap_var
else if D.mem heap_var ctx.local then
end
else if D.mem heap_var ctx.local then begin
AnalysisState.svcomp_may_use_after_free := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "Use-After-Free might occur in current unique thread %a for heap variable %a" ThreadIdDomain.FlagConfiguredTID.pretty current CilType.Varinfo.pretty heap_var
end
end
| `Top ->
AnalysisState.svcomp_may_use_after_free := true;
M.warn ~category:(Behavior behavior) ~tags:[CWE cwe_number] "CurrentThreadId is top. A Use-After-Free might occur for heap variable %a" CilType.Varinfo.pretty heap_var
| `Bot ->
M.warn ~category:MessageCategory.Analyzer "CurrentThreadId is bottom"
Expand All @@ -85,8 +100,10 @@ struct
match ctx.ask (Queries.MayPointTo lval_to_query) with
| a when not (Queries.LS.is_top a) && not (Queries.LS.mem (dummyFunDec.svar, `NoOffset) a) ->
let warn_for_heap_var var =
if D.mem var state then
if D.mem var state then begin
AnalysisState.svcomp_may_use_after_free := true;
M.warn ~category:(Behavior undefined_behavior) ~tags:[CWE cwe_number] "lval (%s) in \"%s\" points to a maybe freed memory region" var.vname transfer_fn_name
end
in
let pointed_to_heap_vars =
Queries.LS.elements a
Expand Down Expand Up @@ -125,9 +142,18 @@ struct
| StartOf lval
| AddrOf lval -> warn_lval_might_contain_freed ~is_double_free transfer_fn_name ctx lval

let side_effect_mem_free ctx freed_heap_vars threadid =
let threadid = G.singleton threadid in
D.iter (fun var -> ctx.sideg var threadid) freed_heap_vars
let side_effect_mem_free ctx freed_heap_vars threadid joined_threads =
let side_effect_globals_to_heap_var heap_var =
let current_globals = ctx.global heap_var in
let joined_threads_to_add = match G.find_opt threadid current_globals with
| Some threads -> ConcDomain.ThreadSet.inter joined_threads threads
| None -> joined_threads
in
michael-schwarz marked this conversation as resolved.
Show resolved Hide resolved
let globals_to_side_effect = G.add threadid joined_threads_to_add current_globals in
ctx.sideg heap_var globals_to_side_effect
in
D.iter side_effect_globals_to_heap_var freed_heap_vars



(* TRANSFER FUNCTIONS *)
Expand Down Expand Up @@ -187,8 +213,9 @@ struct
|> D.of_list
in
(* Side-effect the tid that's freeing all the heap vars collected here *)
side_effect_mem_free ctx pointed_to_heap_vars (get_current_threadid ctx);
D.join state (pointed_to_heap_vars) (* Add all heap vars, which ptr points to, to the state *)
side_effect_mem_free ctx pointed_to_heap_vars (get_current_threadid ctx) (get_joined_threads ctx);
(* Add all heap vars, which ptr points to, to the state *)
D.join state (pointed_to_heap_vars)
| _ -> state
end
| _ -> state
Expand Down
7 changes: 7 additions & 0 deletions src/autoTune.ml
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ let focusOnSpecification () =
| NoOverflow -> (*We focus on integer analysis*)
set_bool "ana.int.def_exc" true;
set_bool "ana.int.interval" true
| ValidFree -> (* Enable the useAfterFree analysis *)
let uafAna = ["useAfterFree"] in
print_endline @@ "Specification: ValidFree -> enabling useAfterFree analysis \"" ^ (String.concat ", " uafAna) ^ "\"";
enableAnalyses uafAna
(* TODO: Finish these two below later *)
| ValidDeref
| ValidMemtrack -> ()

(*Detect enumerations and enable the "ana.int.enums" option*)
exception EnumFound
Expand Down
9 changes: 9 additions & 0 deletions src/framework/analysisState.ml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ let should_warn = ref false
(** Whether signed overflow or underflow happened *)
let svcomp_may_overflow = ref false

(** Whether a Use-After-Free (UAF) happened *)
let svcomp_may_use_after_free = ref false

(** Whether an invalid pointer dereference happened *)
let svcomp_may_invalid_deref = ref false

(** Whether an invalid memtrack happened *)
let svcomp_may_invalid_memtrack = ref false
michael-schwarz marked this conversation as resolved.
Show resolved Hide resolved

(** A hack to see if we are currently doing global inits *)
let global_initialization = ref false

Expand Down
3 changes: 3 additions & 0 deletions src/witness/svcomp.ml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ struct
| UnreachCall _ -> "unreach-call"
| NoOverflow -> "no-overflow"
| NoDataRace -> "no-data-race" (* not yet in SV-COMP/Benchexec *)
| ValidFree -> "valid-free"
| ValidDeref -> "valid-deref"
| ValidMemtrack -> "valid-memtrack"
in
"false(" ^ result_spec ^ ")"
| Unknown -> "unknown"
Expand Down
37 changes: 30 additions & 7 deletions src/witness/svcompSpec.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ type t =
| UnreachCall of string
| NoDataRace
| NoOverflow
| ValidFree
| ValidDeref
| ValidMemtrack

let of_string s =
let s = String.strip s in
let regexp = Str.regexp "CHECK( init(main()), LTL(G ! \\(.*\\)) )" in
if Str.string_match regexp s 0 then
let regexp = Str.regexp "CHECK( init(main()), LTL(G \\(.*\\)) )" in
let regexp_negated = Str.regexp "CHECK( init(main()), LTL(G ! \\(.*\\)) )" in
if Str.string_match regexp_negated s 0 then
let global_not = Str.matched_group 1 s in
if global_not = "data-race" then
NoDataRace
Expand All @@ -23,6 +27,16 @@ let of_string s =
UnreachCall f
else
failwith "Svcomp.Specification.of_string: unknown global not expression"
else if Str.string_match regexp s 0 then
michael-schwarz marked this conversation as resolved.
Show resolved Hide resolved
let global = Str.matched_group 1 s in
if global = "valid-free" then
ValidFree
else if global = "valid-deref" then
ValidDeref
else if global = "valid-memtrack" then
ValidMemtrack
else
failwith "Svcomp.Specification.of_string: unknown global expression"
Comment on lines +30 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with this is that the property file in SV-COMP doesn't contain just one, but all three at once: https://gitlab.com/sosy-lab/benchmarking/sv-benchmarks/-/blob/3d65c76d8521ef5bc79077a31e7b7e41dd077309/c/properties/valid-memsafety.prp.
But we can leave it for now if the idea is to at least test one of them specifically. I can do larger refactoring of the SV-COMP stuff to allow for multi-property.

It just means that as-is, this isn't ready for valid-memsafety.prp from SV-COMP.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest that we discuss and implement support for supporting all three properties, so that we can try and let all relevant analyses for memory safety run together in order to be able to evaluate how we are actually performing in SV-COMP for memory safety.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stuff was run on SV-COMP but that was done per-specification or? because there's no handling of all three simultaneously here still.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We added a workaround by having an internal "memory-safety" property which unites all three. This way we were able to let all three run and be tested simultaneously for the benchmarks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, this will be fixed in a follow-up PR, which will contain commits from the https://github.com/goblint/analyzer/tree/staging_memsafety branch, we just don't want to include them here yet to avoid polluting the history.

else
failwith "Svcomp.Specification.of_string: unknown expression"

Expand All @@ -38,9 +52,18 @@ let of_option () =
of_string s

let to_string spec =
let global_not = match spec with
| UnreachCall f -> "call(" ^ f ^ "())"
| NoDataRace -> "data-race"
| NoOverflow -> "overflow"
let print_output spec_str is_neg =
if is_neg then
Printf.sprintf "CHECK( init(main()), LTL(G ! %s) )" spec_str
else
Printf.sprintf "CHECK( init(main()), LTL(G %s) )" spec_str
in
let spec_str, is_neg = match spec with
| UnreachCall f -> "call(" ^ f ^ "())", true
| NoDataRace -> "data-race", true
| NoOverflow -> "overflow", true
| ValidFree -> "valid-free", false
| ValidDeref -> "valid-deref", false
| ValidMemtrack -> "valid-memtrack", false
in
"CHECK( init(main()), LTL(G ! " ^ global_not ^ ") )"
print_output spec_str is_neg
90 changes: 90 additions & 0 deletions src/witness/witness.ml
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,96 @@ struct
in
(module TaskResult:WitnessTaskResult)
)
| ValidFree ->
let module TrivialArg =
struct
include Arg
let next _ = []
end
in
if not !AnalysisState.svcomp_may_use_after_free then
let module TaskResult =
struct
module Arg = Arg
let result = Result.True
let invariant _ = Invariant.none
let is_violation _ = false
let is_sink _ = false
end
in
(module TaskResult:WitnessTaskResult)
else (
let module TaskResult =
struct
module Arg = TrivialArg
let result = Result.Unknown
let invariant _ = Invariant.none
let is_violation _ = false
let is_sink _ = false
end
in
(module TaskResult:WitnessTaskResult)
)
| ValidDeref ->
let module TrivialArg =
struct
include Arg
let next _ = []
end
in
if not !AnalysisState.svcomp_may_invalid_deref then
let module TaskResult =
struct
module Arg = Arg
let result = Result.True
let invariant _ = Invariant.none
let is_violation _ = false
let is_sink _ = false
end
in
(module TaskResult:WitnessTaskResult)
else (
let module TaskResult =
struct
module Arg = TrivialArg
let result = Result.Unknown
let invariant _ = Invariant.none
let is_violation _ = false
let is_sink _ = false
end
in
(module TaskResult:WitnessTaskResult)
)
| ValidMemtrack ->
let module TrivialArg =
struct
include Arg
let next _ = []
end
in
if not !AnalysisState.svcomp_may_invalid_memtrack then
let module TaskResult =
struct
module Arg = Arg
let result = Result.True
let invariant _ = Invariant.none
let is_violation _ = false
let is_sink _ = false
end
in
(module TaskResult:WitnessTaskResult)
else (
let module TaskResult =
struct
module Arg = TrivialArg
let result = Result.Unknown
let invariant _ = Invariant.none
let is_violation _ = false
let is_sink _ = false
end
in
(module TaskResult:WitnessTaskResult)
)


let write entrystates =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//PARAM: --set ana.activated[+] useAfterFree --set ana.activated[+] threadJoins
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

int* gptr;

// Mutex to ensure we don't get race warnings, but the UAF warnings we actually care about
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void *t_use(void* p) {
pthread_mutex_lock(&mtx);
*gptr = 0; //NOWARN
michael-schwarz marked this conversation as resolved.
Show resolved Hide resolved
pthread_mutex_unlock(&mtx);
}

int main() {
gptr = malloc(sizeof(int));
*gptr = 42;

pthread_t using_thread;
pthread_create(&using_thread, NULL, t_use, NULL);

// Join using_thread before freeing gptr in the main thread
pthread_join(using_thread, NULL);

pthread_mutex_lock(&mtx);
*gptr = 43; //NOWARN
free(gptr); //NOWARN
pthread_mutex_unlock(&mtx);

return 0;
}