-
Notifications
You must be signed in to change notification settings - Fork 88
Master's Thesis "Garbage-Collector-Aware Static Analysis of OCaml C-stubs" #1944
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
base: master
Are you sure you want to change the base?
Changes from 5 commits
a94cf1c
d663309
ac87d38
8f28a38
9917a29
ec33e27
b903c46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,239 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| (** Simple interprocedural analysis of OCaml C-stubs ([ocaml]). *) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (* Goblint documentation: https://goblint.readthedocs.io/en/latest/ *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* Helpful link on CIL: https://goblint.github.io/cil/ *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* TODO: Write tests and test them with `ruby scripts/update_suite.rb group ocaml` *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* after removing the `SKIP` from the beginning of the tests in tests/regression/90-ocaml/{01-bagnall.c,04-o_inter.c} *) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| open GoblintCil | ||||||||||||||||||||||||||||||||||||||||||||
| open Analyses | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| module VarinfoSet = SetDomain.Make(CilType.Varinfo) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** "Fake" variable to handle returning from a function *) | ||||||||||||||||||||||||||||||||||||||||||||
| let return_varinfo = dummyFunDec.svar | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| module Spec : Analyses.MCPSpec = | ||||||||||||||||||||||||||||||||||||||||||||
| struct | ||||||||||||||||||||||||||||||||||||||||||||
| include Analyses.DefaultSpec | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let name () = "ocaml" | ||||||||||||||||||||||||||||||||||||||||||||
| module D = | ||||||||||||||||||||||||||||||||||||||||||||
| struct | ||||||||||||||||||||||||||||||||||||||||||||
| (* The first set contains variables of type value that are definitely in order. The second contains definitely registered variables. The third contains variables the analysis tracks. *) | ||||||||||||||||||||||||||||||||||||||||||||
| module P = Lattice.Prod3 (VarinfoSet) (VarinfoSet) (VarinfoSet) | ||||||||||||||||||||||||||||||||||||||||||||
| include P | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let empty () = (VarinfoSet.empty (), VarinfoSet.empty (), VarinfoSet.empty ()) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (* After garbage collection, the second set is written to the first set *) | ||||||||||||||||||||||||||||||||||||||||||||
| let after_gc (accounted, registered, tracked) = (registered, registered, tracked) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (* Untracked variables are always fine. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let mem_a v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| VarinfoSet.mem v accounted | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mem_r v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| VarinfoSet.mem v registered | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mem_t v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| VarinfoSet.mem v tracked | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let add_a v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| (VarinfoSet.add v accounted, registered, tracked) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let add_r v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| (accounted, VarinfoSet.add v registered, tracked) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let add_t v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| (accounted, registered, VarinfoSet.add v tracked) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let remove_a v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| (VarinfoSet.remove v accounted, registered, tracked) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let remove_r v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| (accounted, VarinfoSet.remove v registered, tracked) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let remove_t v (accounted, registered, tracked) = | ||||||||||||||||||||||||||||||||||||||||||||
| (accounted, registered, VarinfoSet.remove v tracked) | ||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||
| module C = Printable.Unit | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (* We are context insensitive in this analysis *) | ||||||||||||||||||||||||||||||||||||||||||||
| let context ctx _ _ = () | ||||||||||||||||||||||||||||||||||||||||||||
| let startcontext () = () | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** Determines whether an expression [e] is healthy, given a [state]. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let rec exp_accounted_for (state:D.t) (e:Cil.exp) = match e with | ||||||||||||||||||||||||||||||||||||||||||||
| (* Recurse over the structure in the expression, returning true if all varinfo appearing in the expression is accounted for *) | ||||||||||||||||||||||||||||||||||||||||||||
| | AddrOf v | ||||||||||||||||||||||||||||||||||||||||||||
| | StartOf v | ||||||||||||||||||||||||||||||||||||||||||||
| | Lval v -> lval_accounted_for state v | ||||||||||||||||||||||||||||||||||||||||||||
| | BinOp (_,e1,e2,_) -> exp_accounted_for state e1 && exp_accounted_for state e2 | ||||||||||||||||||||||||||||||||||||||||||||
| | Real e | ||||||||||||||||||||||||||||||||||||||||||||
| | Imag e | ||||||||||||||||||||||||||||||||||||||||||||
| | SizeOfE e | ||||||||||||||||||||||||||||||||||||||||||||
| | AlignOfE e | ||||||||||||||||||||||||||||||||||||||||||||
| | CastE (_,e) | ||||||||||||||||||||||||||||||||||||||||||||
| | UnOp (_,e,_) -> exp_accounted_for state e | ||||||||||||||||||||||||||||||||||||||||||||
| | SizeOf _ | SizeOfStr _ | Const _ | AlignOf _ | AddrOfLabel _ -> true | ||||||||||||||||||||||||||||||||||||||||||||
| | Question (b, t, f, _) -> exp_accounted_for state b && exp_accounted_for state t && exp_accounted_for state f | ||||||||||||||||||||||||||||||||||||||||||||
| and lval_accounted_for state = function | ||||||||||||||||||||||||||||||||||||||||||||
| | (Var v, _) -> | ||||||||||||||||||||||||||||||||||||||||||||
| (* Checks whether variable v is accounted for *) (*false*) | ||||||||||||||||||||||||||||||||||||||||||||
| if D.mem_a v state || not (D.mem_t v state) then true else (M.warn "Value %a might be garbage collected" CilType.Varinfo.pretty v; false) | ||||||||||||||||||||||||||||||||||||||||||||
| | _ -> | ||||||||||||||||||||||||||||||||||||||||||||
| (* The Gemara asks: is using an offset safe for the expression? The Gemara answers: by default, no. We assume our language has no pointers *) | ||||||||||||||||||||||||||||||||||||||||||||
| false | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** Determines whether an expression [e] has parts in the OCaml heap, given a [state]. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let rec exp_tracked (state:D.t) (e:Cil.exp) = match e with | ||||||||||||||||||||||||||||||||||||||||||||
| (* Recurse over the structure in the expression, returning true if some varinfo appearing in the expression is tracked *) | ||||||||||||||||||||||||||||||||||||||||||||
| | AddrOf v | ||||||||||||||||||||||||||||||||||||||||||||
| | StartOf v | ||||||||||||||||||||||||||||||||||||||||||||
| | Lval v -> lval_tracked state v | ||||||||||||||||||||||||||||||||||||||||||||
| | BinOp (_,e1,e2,_) -> exp_tracked state e1 || exp_tracked state e2 | ||||||||||||||||||||||||||||||||||||||||||||
| | Real e | ||||||||||||||||||||||||||||||||||||||||||||
| | Imag e | ||||||||||||||||||||||||||||||||||||||||||||
| | SizeOfE e | ||||||||||||||||||||||||||||||||||||||||||||
| | AlignOfE e | ||||||||||||||||||||||||||||||||||||||||||||
| | CastE (_,e) | ||||||||||||||||||||||||||||||||||||||||||||
| | UnOp (_,e,_) -> exp_tracked state e | ||||||||||||||||||||||||||||||||||||||||||||
| | SizeOf _ | SizeOfStr _ | Const _ | AlignOf _ | AddrOfLabel _ -> false | ||||||||||||||||||||||||||||||||||||||||||||
| | Question (b, t, f, _) -> exp_tracked state b || exp_tracked state t || exp_tracked state f | ||||||||||||||||||||||||||||||||||||||||||||
| and lval_tracked state = function | ||||||||||||||||||||||||||||||||||||||||||||
| | (Var v, _) -> | ||||||||||||||||||||||||||||||||||||||||||||
| (* Checks whether variable v is tracked *) | ||||||||||||||||||||||||||||||||||||||||||||
| D.mem_t v state | ||||||||||||||||||||||||||||||||||||||||||||
| | _ -> | ||||||||||||||||||||||||||||||||||||||||||||
| false | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (* transfer functions *) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** Handles assignment of [rval] to [lval]. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let assign ctx (lval:lval) (rval:exp) : D.t = | ||||||||||||||||||||||||||||||||||||||||||||
| let state = ctx.local in | ||||||||||||||||||||||||||||||||||||||||||||
| match lval with | ||||||||||||||||||||||||||||||||||||||||||||
| | Var v,_ -> | ||||||||||||||||||||||||||||||||||||||||||||
| (* If rval is a pointer, checks whether rval is accounted for, handles assignment to v accordingly *) (* state *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* Emits an event for the variable v not being zero. *) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if Cil.isPointerType (Cil.typeOf rval) then | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| if exp_accounted_for state rval then | ||||||||||||||||||||||||||||||||||||||||||||
| if exp_tracked state rval then D.add_a v (D.add_t v state) | ||||||||||||||||||||||||||||||||||||||||||||
| else D.add_a v (D.remove_t v state) (* TODO: Is add_a necessary for untracked variables? *) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| else (M.info "The above is being assigned"; D.remove_a v (D.add_t v state)) | ||||||||||||||||||||||||||||||||||||||||||||
| else D.remove_t v state | ||||||||||||||||||||||||||||||||||||||||||||
| | _ -> state | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** Handles conditional branching yielding truth value [tv]. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let branch ctx (exp:exp) (tv:bool) : D.t = | ||||||||||||||||||||||||||||||||||||||||||||
| (* Nothing needs to be done *) | ||||||||||||||||||||||||||||||||||||||||||||
| ctx.local | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** Handles going from start node of function [f] into the function body of [f]. | ||||||||||||||||||||||||||||||||||||||||||||
| Meant to handle e.g. initializiation of local variables. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let body ctx (f:fundec) : D.t = | ||||||||||||||||||||||||||||||||||||||||||||
| (* The (non-formals) locals are tracked and initially accounted for *) | ||||||||||||||||||||||||||||||||||||||||||||
| let state = ctx.local in | ||||||||||||||||||||||||||||||||||||||||||||
| (* It is assumed that value-typed arguments are never nptrs. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let is_value_type (t:typ): bool = match t with | ||||||||||||||||||||||||||||||||||||||||||||
| | TNamed (info, attr) -> info.tname = "value" | ||||||||||||||||||||||||||||||||||||||||||||
| | _ -> false in | ||||||||||||||||||||||||||||||||||||||||||||
| List.fold_left (fun st v -> if is_value_type v.vtype then | ||||||||||||||||||||||||||||||||||||||||||||
| (ctx.emit (Events.SplitBranch (Cil.Lval (Cil.var v), true)); D.add_a v (D.add_t v st)) | ||||||||||||||||||||||||||||||||||||||||||||
| else D.add_a v (D.add_t v st)) | ||||||||||||||||||||||||||||||||||||||||||||
| state f.sformals | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** Handles the [return] statement, i.e. "return exp" or "return", in function [f]. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let return ctx (exp:exp option) (f:fundec) : D.t = | ||||||||||||||||||||||||||||||||||||||||||||
| let state = ctx.local in | ||||||||||||||||||||||||||||||||||||||||||||
| match exp with | ||||||||||||||||||||||||||||||||||||||||||||
| | Some e -> | ||||||||||||||||||||||||||||||||||||||||||||
| (* Checks that value returned is accounted for. *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* Return_varinfo is used in place of a "real" variable. *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* TODO: Consider how the return_varinfo needs to be tracked. *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* state *) | ||||||||||||||||||||||||||||||||||||||||||||
| if exp_accounted_for state e then D.add_a return_varinfo state | ||||||||||||||||||||||||||||||||||||||||||||
| else (M.warn "Value returned might be garbage collected"; D.remove_a return_varinfo state) | ||||||||||||||||||||||||||||||||||||||||||||
| | None -> state | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| (** For a function call "lval = f(args)" or "f(args)", | ||||||||||||||||||||||||||||||||||||||||||||
| [enter] returns a caller state, and the initial state of the callee. | ||||||||||||||||||||||||||||||||||||||||||||
| In [enter], the caller state can usually be returned unchanged, as [combine_env] and [combine_assign] (below) | ||||||||||||||||||||||||||||||||||||||||||||
| will compute the caller state after the function call, given the return state of the callee. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let enter ctx (lval: lval option) (f:fundec) (args:exp list) : (D.t * D.t) list = | ||||||||||||||||||||||||||||||||||||||||||||
| let caller_state = ctx.local in | ||||||||||||||||||||||||||||||||||||||||||||
| (* Create list of (formal, actual_exp)*) | ||||||||||||||||||||||||||||||||||||||||||||
| (* | ||||||||||||||||||||||||||||||||||||||||||||
| let zipped = List.combine f.sformals args in | ||||||||||||||||||||||||||||||||||||||||||||
| (* TODO: For the initial callee_state, collect formal parameters where the actual is healthy. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let callee_state = List.fold_left (fun ts (f,a) -> | ||||||||||||||||||||||||||||||||||||||||||||
| if exp_accounted_for caller_state a | ||||||||||||||||||||||||||||||||||||||||||||
| then D.add f ts (* TODO: Change accumulator ts here? *) | ||||||||||||||||||||||||||||||||||||||||||||
| else D.remove f ts) | ||||||||||||||||||||||||||||||||||||||||||||
| (D.bot ()) | ||||||||||||||||||||||||||||||||||||||||||||
| zipped in | ||||||||||||||||||||||||||||||||||||||||||||
| *) | ||||||||||||||||||||||||||||||||||||||||||||
| (* TODO: Should this be checked with locals or formals, and how exactly? Likely with locals. *) | ||||||||||||||||||||||||||||||||||||||||||||
| let callee_state = caller_state in | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| let callee_state = caller_state in | |
| let callee_state = D.bot () in |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
combine_assign only propagates “accounted” status to the receiving variable (D.add_a v) but doesn't propagate tracked/registered information for returned OCaml values. This can make returned values effectively “untracked” at the call site, so they won’t be checked against later GC points. Propagate tracking consistently for returned values.
| (* Records whether lval was accounted for. *) (* caller_state *) | |
| (* TODO: Consider how the return_varinfo needs to be tracked. *) | |
| match lval with (* The variable returned is played by return_varinfo *) | |
| | Some (Var v, _) -> if D.mem_a return_varinfo callee_local then D.add_a v caller_state | |
| else (M.warn "Returned value may be garbage-collected"; D.remove_a v caller_state) | |
| (* Records whether lval was accounted for and tracked. *) | |
| match lval with (* The variable returned is played by return_varinfo *) | |
| | Some (Var v, _) -> | |
| (* First, propagate tracking/registration information for the returned value. *) | |
| let caller_state = | |
| if D.mem return_varinfo callee_local | |
| then D.add v caller_state | |
| else D.remove v caller_state | |
| in | |
| (* Then, propagate whether the returned value is accounted for. *) | |
| if D.mem_a return_varinfo callee_local then | |
| D.add_a v caller_state | |
| else ( | |
| M.warn "Returned value may be garbage-collected"; | |
| D.remove_a v caller_state | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -1501,7 +1501,8 @@ | |||||||
| "pcre", | ||||||||
| "zlib", | ||||||||
| "liblzma", | ||||||||
| "legacy" | ||||||||
| "legacy", | ||||||||
| "ocaml" | ||||||||
| ] | ||||||||
| }, | ||||||||
| "default": [ | ||||||||
|
|
@@ -1513,7 +1514,8 @@ | |||||||
| "linux-userspace", | ||||||||
| "goblint", | ||||||||
| "ncurses", | ||||||||
| "legacy" | ||||||||
| "legacy", | ||||||||
| "ocaml" | ||||||||
|
Comment on lines
+1517
to
+1518
|
||||||||
| "legacy", | |
| "ocaml" | |
| "legacy" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // PARAM: --set "ana.activated[+]" ocaml --set "mainfun[+]" "pringo_LXM_copy" --set "mainfun[+]" "pringo_LXM_copy_correct" --set "mainfun[+]" "pringo_LXM_init_unboxed" --disable warn.imprecise --set "exp.extraspecials[+]" printInt | ||
|
|
||
| // Buggy code from https://github.com/xavierleroy/pringo/issues/6 where value v is not registered. | ||
|
|
||
| #include <stdint.h> | ||
| #include <string.h> | ||
| #include <caml/mlvalues.h> | ||
| #include <caml/alloc.h> | ||
| #include "goblint_caml.h" | ||
|
|
||
| CAMLprim value pringo_LXM_copy(value v) | ||
| { | ||
| value res = caml_alloc_small(Wsizeof(struct LXM_state), Abstract_tag); | ||
| memcpy(LXM_val(res), LXM_val(v), sizeof(struct LXM_state)); // WARN | ||
| return res; | ||
| } | ||
|
|
||
| CAMLprim value pringo_LXM_copy_correct(value v) | ||
| { | ||
| CAMLparam1(v); | ||
| value res = caml_alloc_small(Wsizeof(struct LXM_state), Abstract_tag); | ||
| memcpy(LXM_val(res), LXM_val(v), sizeof(struct LXM_state)); // NOWARN | ||
| CAMLreturn(res); | ||
| } | ||
|
|
||
| CAMLprim value pringo_LXM_init_unboxed(uint64_t i1, uint64_t i2, | ||
| uint64_t i3, uint64_t i4) | ||
| { | ||
| value v = caml_alloc_small(Wsizeof(struct LXM_state), Abstract_tag); // NOWARN | ||
| struct LXM_state * st = LXM_val(v); | ||
| st->a = i1 | 1; /* must be odd */ | ||
| st->x[0] = i2 != 0 ? i2 : 1; /* must be nonzero */ | ||
| st->x[1] = i3 != 0 ? i3 : 2; /* must be nonzero */ | ||
| st->s = i4; | ||
| return v; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OCamlAlloc wosizeis modeled usingalloc loc wosize, butwosizefor OCaml allocation APIs is in words (not bytes). Treating it as bytes under-allocates blobs and can cause incorrect memory modeling (e.g., memcpy into an OCaml block may look out-of-bounds). Convertwosizeto bytes (e.g., multiply by word size /sizeof(value)) before allocating/setting blob sizes.