|
| 1 | +(** Simple interprocedural analysis of OCaml C-stubs ([ocaml]). *) |
| 2 | + |
| 3 | +(* Goblint documentation: https://goblint.readthedocs.io/en/latest/ *) |
| 4 | +(* Helpful link on CIL: https://goblint.github.io/cil/ *) |
| 5 | +(* TODO: Write tests and test them with `ruby scripts/update_suite.rb group ocaml` *) |
| 6 | +(* after removing the `SKIP` from the beginning of the tests in tests/regression/90-ocaml/{01-bagnall.c,04-o_inter.c} *) |
| 7 | + |
| 8 | +open GoblintCil |
| 9 | +open Analyses |
| 10 | + |
| 11 | +module VarinfoSet = SetDomain.Make(CilType.Varinfo) |
| 12 | + |
| 13 | +(* Use to check if a specific function is a sink / source *) |
| 14 | +(* Sources take value-typed arguments *) |
| 15 | +(* Sinks may trigger garbage collection *) |
| 16 | +let is_sink varinfo = Cil.hasAttribute "ocaml_sink" varinfo.vattr |
| 17 | +let is_source varinfo = Cil.hasAttribute "ocaml_source" varinfo.vattr |
| 18 | + |
| 19 | + |
| 20 | +(** "Fake" variable to handle returning from a function *) |
| 21 | +let return_varinfo = dummyFunDec.svar |
| 22 | + |
| 23 | +module Spec : Analyses.MCPSpec = |
| 24 | +struct |
| 25 | + include Analyses.DefaultSpec |
| 26 | + |
| 27 | + let name () = "ocaml" |
| 28 | + module D = |
| 29 | + struct |
| 30 | + (* The first set contains variables of type value that are definitely in order. The second contains definitely registered variables. *) |
| 31 | + module P = Lattice.Prod (VarinfoSet) (VarinfoSet) |
| 32 | + include P |
| 33 | + |
| 34 | + let empty () = (VarinfoSet.empty (), VarinfoSet.empty ()) |
| 35 | + |
| 36 | + (* After garbage collection, the second set is written to the first set *) |
| 37 | + let after_gc (accounted, registered) = (registered, registered) |
| 38 | + |
| 39 | + let mem_a v (accounted, registered) = |
| 40 | + VarinfoSet.mem v accounted |
| 41 | + |
| 42 | + let mem_r v (accounted, registered) = |
| 43 | + VarinfoSet.mem v registered |
| 44 | + |
| 45 | + let add_a v (accounted, registered) = |
| 46 | + (VarinfoSet.add v accounted, registered) |
| 47 | + |
| 48 | + let add_r v (accounted, registered) = |
| 49 | + (accounted, VarinfoSet.add v registered) |
| 50 | + |
| 51 | + let remove_a v (accounted, registered) = |
| 52 | + (VarinfoSet.remove v accounted, registered) |
| 53 | + |
| 54 | + let remove_r v (accounted, registered) = |
| 55 | + (accounted, VarinfoSet.remove v registered) |
| 56 | + end |
| 57 | + module C = Printable.Unit |
| 58 | + |
| 59 | + (* We are context insensitive in this analysis *) |
| 60 | + let context ctx _ _ = () |
| 61 | + let startcontext () = () |
| 62 | + |
| 63 | + |
| 64 | + (** Determines whether an expression [e] is healthy, given a [state]. *) |
| 65 | + let rec exp_accounted_for (state:D.t) (e:Cil.exp) = match e with |
| 66 | + (* Recurse over the structure in the expression, returning true if all varinfo appearing in the expression is accounted for *) |
| 67 | + | AddrOf v |
| 68 | + | StartOf v |
| 69 | + | Lval v -> lval_accounted_for state v |
| 70 | + | BinOp (_,e1,e2,_) -> exp_accounted_for state e1 && exp_accounted_for state e2 |
| 71 | + | Real e |
| 72 | + | Imag e |
| 73 | + | SizeOfE e |
| 74 | + | AlignOfE e |
| 75 | + | CastE (_,e) |
| 76 | + | UnOp (_,e,_) -> exp_accounted_for state e |
| 77 | + | SizeOf _ | SizeOfStr _ | Const _ | AlignOf _ | AddrOfLabel _ -> false |
| 78 | + | Question (b, t, f, _) -> exp_accounted_for state b && exp_accounted_for state t && exp_accounted_for state f |
| 79 | + and lval_accounted_for state = function |
| 80 | + | (Var v, _) -> |
| 81 | + (* Checks whether variable v is accounted for *) (*false*) |
| 82 | + if D.mem_a v state then true else let _ = M.warn "Value %a might be garbage collected" CilType.Varinfo.pretty v in false |
| 83 | + | _ -> |
| 84 | + (* The Gemara asks: is using an offset safe for the expression? The Gemara answers: by default, no. We assume our language has no pointers *) |
| 85 | + false |
| 86 | + |
| 87 | + (* transfer functions *) |
| 88 | + |
| 89 | + (** Handles assignment of [rval] to [lval]. *) |
| 90 | + let assign ctx (lval:lval) (rval:exp) : D.t = |
| 91 | + let state = ctx.local in |
| 92 | + match lval with |
| 93 | + | Var v,_ -> |
| 94 | + (* If lval is of type value, checks whether rval is accounted for, handles assignment to v accordingly *) (* state *) |
| 95 | + if exp_accounted_for state rval then D.add_a v state |
| 96 | + else D.remove_a v state |
| 97 | + | _ -> state |
| 98 | + |
| 99 | + (** Handles conditional branching yielding truth value [tv]. *) |
| 100 | + let branch ctx (exp:exp) (tv:bool) : D.t = |
| 101 | + (* Nothing needs to be done *) |
| 102 | + ctx.local |
| 103 | + |
| 104 | + (** Handles going from start node of function [f] into the function body of [f]. |
| 105 | + Meant to handle e.g. initializiation of local variables. *) |
| 106 | + let body ctx (f:fundec) : D.t = |
| 107 | + (* The (non-formals) locals are initially accounted for *) |
| 108 | + let state = ctx.local in |
| 109 | + List.fold_left (fun st v -> D.add_a v st) state f.sformals |
| 110 | + |
| 111 | + (** Handles the [return] statement, i.e. "return exp" or "return", in function [f]. *) |
| 112 | + let return ctx (exp:exp option) (f:fundec) : D.t = |
| 113 | + let state = ctx.local in |
| 114 | + match exp with |
| 115 | + | Some e -> |
| 116 | + (* Checks that value returned is accounted for. *) |
| 117 | + (* Return_varinfo is used in place of a "real" variable. *) |
| 118 | + (* state *) |
| 119 | + if exp_accounted_for state e then D.add_a return_varinfo state |
| 120 | + else let _ = M.warn "Value returned might be garbage collected" in D.remove_a return_varinfo state |
| 121 | + | None -> state |
| 122 | + |
| 123 | + (** For a function call "lval = f(args)" or "f(args)", |
| 124 | + [enter] returns a caller state, and the initial state of the callee. |
| 125 | + In [enter], the caller state can usually be returned unchanged, as [combine_env] and [combine_assign] (below) |
| 126 | + will compute the caller state after the function call, given the return state of the callee. *) |
| 127 | + let enter ctx (lval: lval option) (f:fundec) (args:exp list) : (D.t * D.t) list = |
| 128 | + let caller_state = ctx.local in |
| 129 | + (* Create list of (formal, actual_exp)*) |
| 130 | + (* |
| 131 | + let zipped = List.combine f.sformals args in |
| 132 | + (* TODO: For the initial callee_state, collect formal parameters where the actual is healthy. *) |
| 133 | + let callee_state = List.fold_left (fun ts (f,a) -> |
| 134 | + if exp_accounted_for caller_state a |
| 135 | + then D.add f ts (* TODO: Change accumulator ts here? *) |
| 136 | + else D.remove f ts) |
| 137 | + (D.bot ()) |
| 138 | + zipped in |
| 139 | + *) |
| 140 | + (* TODO: Should this be checked with locals or formals, and how exactly? Likely with locals. *) |
| 141 | + let callee_state = caller_state in |
| 142 | + (* first component is state of caller, second component is state of callee *) |
| 143 | + [caller_state, callee_state] |
| 144 | + |
| 145 | + (** For a function call "lval = f(args)" or "f(args)", |
| 146 | + computes the global environment state of the caller after the call. |
| 147 | + Argument [callee_local] is the state of [f] at its return node. *) |
| 148 | + let combine_env ctx (lval:lval option) fexp (f:fundec) (args:exp list) fc (callee_local:D.t) (f_ask: Queries.ask): D.t = |
| 149 | + (* Nothing needs to be done *) |
| 150 | + ctx.local |
| 151 | + |
| 152 | + (** For a function call "lval = f(args)" or "f(args)", |
| 153 | + computes the state of the caller after assigning the return value from the call. |
| 154 | + Argument [callee_local] is the state of [f] at its return node. *) |
| 155 | + let combine_assign ctx (lval:lval option) fexp (f:fundec) (args:exp list) fc (callee_local:D.t) (f_ask: Queries.ask): D.t = |
| 156 | + let caller_state = ctx.local in |
| 157 | + (* Records whether lval was accounted for. *) (* caller_state *) |
| 158 | + match lval with (* The variable returned is played by return_varinfo *) |
| 159 | + | Some (Var v, _) -> if D.mem_a return_varinfo callee_local then D.add_a v caller_state |
| 160 | + else let _ = M.warn "Returned value may be garbage-collected" in D.remove_a v caller_state |
| 161 | + | _ -> caller_state |
| 162 | + |
| 163 | + (** For a call to a _special_ function f "lval = f(args)" or "f(args)", |
| 164 | + computes the caller state after the function call. |
| 165 | + For this analysis, source and sink functions will be considered _special_ and have to be treated here. *) |
| 166 | + let special ctx (lval: lval option) (f:varinfo) (arglist:exp list) : D.t = |
| 167 | + let caller_state = ctx.local in |
| 168 | + (* TODO: Check if f is a sink / source and handle it appropriately *) |
| 169 | + (* To warn about a potential issue in the code, use M.warn. *) |
| 170 | + (* caller_state *) |
| 171 | + let desc = LibraryFunctions.find f in |
| 172 | + match desc.special arglist with |
| 173 | + (* TODO: Add a source function that registers variables and add the non-buggy Bagnall code to the test. *) |
| 174 | + | OCamlAlloc size_exp -> |
| 175 | + (* Garbage collection may trigger here and overwrite unregistered variables *) |
| 176 | + let _ = M.info "Garbage collection triggers" in (match lval with |
| 177 | + | Some (Var v, _) -> D.add_a v (D.after_gc caller_state) |
| 178 | + | _ -> |
| 179 | + (* if not (List.for_all (exp_accounted_for caller_state) arglist) then let _ = M.warn "GC might delete value" in D.empty () else *) D.empty () |
| 180 | + ) |
| 181 | + | _ -> |
| 182 | + List.iter (fun e -> ignore (exp_accounted_for caller_state e)) arglist; (* Just to trigger warnings for arguments passed to sinks/sources *) |
| 183 | + if is_source f then match lval with (* Assigning the source's result makes the variable accounted for. *) |
| 184 | + | Some (Var v, _) -> D.add_a v caller_state |
| 185 | + | _ -> caller_state |
| 186 | + else |
| 187 | + if is_sink f then (* Warns if unaccounted variables reach the function. Empties the state of unregistered variables. *) |
| 188 | + let _ = M.info "Garbage collection triggers" in match lval with |
| 189 | + | Some (Var v, _) -> D.add_a v (D.after_gc caller_state) |
| 190 | + | _ -> |
| 191 | + (* if not (List.for_all (exp_accounted_for caller_state) arglist) then let _ = M.warn "GC might delete value" in D.empty () else *) |
| 192 | + D.after_gc caller_state |
| 193 | + else caller_state |
| 194 | + |
| 195 | + (* You may leave these alone *) |
| 196 | + let startstate v = D.bot () |
| 197 | + let threadenter ctx ~multiple lval f args = [D.top ()] |
| 198 | + let threadspawn ctx ~multiple lval f args fctx = ctx.local |
| 199 | + let exitstate v = D.top () |
| 200 | +end |
| 201 | + |
| 202 | +let _ = |
| 203 | + MCP.register_analysis (module Spec : MCPSpec) |
0 commit comments