···11open Libbpf
22open Libbpf_maps
3344+type format = Csv | Json
55+46let obj_path = "opentrace.bpf.o"
57let obj_file = [%blob "opentrace.bpf.o"]
68···810 [
911 "tracepoint__syscalls__sys_enter_openat";
1012 "tracepoint__syscalls__sys_enter_openat2";
1111- (* "tracepoint__syscalls__sys_enter_open"; *)
1313+ "tracepoint__syscalls__sys_exit_openat";
1414+ "tracepoint__syscalls__sys_exit_openat2";
1215 ]
13161717+let json_to_lexemes json : Jsonm.lexeme list =
1818+ let rec loop acc = function
1919+ | `String _ as s -> s :: acc
2020+ | `Int i -> `Float (Int.to_float i) :: acc
2121+ | `O assoc ->
2222+ let lexemes =
2323+ List.fold_left
2424+ (fun vacc (k, v) -> loop (`Name k :: vacc) v)
2525+ (`Os :: acc) assoc
2626+ in
2727+ `Oe :: lexemes
2828+ in
2929+ loop [] json |> List.rev
3030+1431module Open_event = struct
1532 open Ctypes
1633···3148 let t : t structure typ = Ctypes.structure "event"
3249 let ( -: ) ty label = Ctypes.field t label ty
3350 let pid = uint32_t -: "e_pid"
5151+ let cgid = uint64_t -: "e_cgid"
5252+ let comm = array 16 char -: "e_comm"
3453 let kind = int -: "e_kind"
3554 let flags = int -: "e_flags"
3655 let mode = uint32_t -: "e_mode"
3756 let filename = array 256 char -: "e_filename"
5757+ let ret = int -: "e_ret"
3858 let () = seal t
39594060 let char_array_as_string a =
···4969 with Exit -> Buffer.contents b
50705171 let get_pid s = getf s pid |> Unsigned.UInt32.to_int
7272+ let get_cgid s = getf s cgid |> Unsigned.UInt64.to_int64
7373+ let get_comm s = getf s comm |> char_array_as_string
5274 let get_flags s = getf s flags
5375 let get_mode s = getf s mode |> Unsigned.UInt32.to_int
5476 let get_fname s = getf s filename |> char_array_as_string
5577 let get_kind s = getf s kind |> kind_of_int
7878+ let get_ret s = getf s ret
7979+ let csv_header = "pid,cgid,comm,kind,flags,mode,filename,return\n"
8080+8181+ let to_csv_row event =
8282+ Format.sprintf "%i,%Ld,%S,%s,%i,%i,\"%s\",%i%!" (get_pid event)
8383+ (get_cgid event) (get_comm event)
8484+ (get_kind event |> kind_to_string)
8585+ (get_flags event) (get_mode event) (get_fname event) (get_ret event)
8686+8787+ let to_json event =
8888+ `O
8989+ [
9090+ ("pid", `Int (get_pid event));
9191+ ("cgid", `Int (Int64.to_int @@ get_cgid event));
9292+ ("comm", `String (get_comm event));
9393+ ("kind", `String (get_kind event |> kind_to_string));
9494+ ("flags", `Int (get_flags event));
9595+ ("mode", `Int (get_mode event));
9696+ ("fname", `String (get_fname event));
9797+ ("ret", `Int (get_ret event));
9898+ ]
5699end
571005858-let () =
101101+let run_ring_buffer bpf_callback =
59102 let dir = Filename.temp_dir "opentrace-" "" in
60103 let full_obj_path = Filename.concat dir obj_path in
6161- Out_channel.with_open_bin full_obj_path (fun oc -> Out_channel.output_string oc obj_file);
104104+ Out_channel.with_open_bin full_obj_path (fun oc ->
105105+ Out_channel.output_string oc obj_file);
106106+ with_bpf_object_open_load_link ~obj_path:full_obj_path ~program_names
107107+ bpf_callback
108108+109109+let ringbuffer_polling_callback ~poll rb_cb exit_cb =
62110 let bpf_callback obj _links =
63111 (* Set signal handlers *)
64112 let exitting = ref true in
···66114 Sys.(set_signal sigint sig_handler);
67115 Sys.(set_signal sigterm sig_handler);
681166969- (* Print header *)
7070- Format.printf "pid,kind,flags,mode,filename\n";
7171-72117 let map = Libbpf.bpf_object_find_map_by_name obj "rb" in
73118 let callback : RingBuffer.callback =
74119 fun _ data _ ->
75120 let event = Ctypes.(!@(from_voidp Open_event.t data)) in
7676- Format.printf "%i,%s,%i,%i,\"%s\"\n%!" (Open_event.get_pid event)
7777- (Open_event.get_kind event |> Open_event.kind_to_string)
7878- (Open_event.get_flags event)
7979- (Open_event.get_mode event)
8080- (Open_event.get_fname event);
8181- 0
121121+ rb_cb event
82122 in
83123 RingBuffer.init map ~callback @@ fun rb ->
84124 while !exitting do
8585- Unix.sleepf 1.0;
86125 let _ : int = RingBuffer.poll rb ~timeout:1 in
8787- ()
126126+ exit_cb exitting;
127127+ Unix.sleepf poll
88128 done
89129 in
9090- with_bpf_object_open_load_link ~obj_path:full_obj_path ~program_names bpf_callback
130130+ bpf_callback
131131+132132+let all poll no_header =
133133+ if no_header then () else Format.printf "%s" Open_event.csv_header;
134134+ let callback event =
135135+ Format.printf "%s\n%!" (Open_event.to_csv_row event);
136136+ 0
137137+ in
138138+ let bpf_callback = ringbuffer_polling_callback ~poll callback (fun _ -> ()) in
139139+ run_ring_buffer bpf_callback
140140+141141+let exec format output user poll (prog, args) =
142142+ let output =
143143+ match output with
144144+ | Some file -> file
145145+ | None ->
146146+ let ext = match format with Csv -> "csv" | Json -> "json" in
147147+ "trace." ^ ext
148148+ in
149149+ let uid =
150150+ match user with
151151+ | None -> Unix.getenv "SUDO_UID" |> int_of_string
152152+ | Some user -> (
153153+ match int_of_string_opt user with
154154+ | Some uid -> uid
155155+ | None -> (Unix.getpwnam user).pw_uid)
156156+ in
157157+ assert (uid <> 0);
158158+ let start_process = Condition.create () in
159159+ let mutex = Mutex.create () in
160160+ let pid = Atomic.make None in
161161+ let exit_status = Atomic.make None in
162162+ let _domain =
163163+ Domain.spawn @@ fun () ->
164164+ Eio_main.run @@ fun env ->
165165+ Mutex.lock mutex;
166166+ Condition.wait start_process mutex;
167167+ Eio.Switch.run @@ fun sw ->
168168+ let p =
169169+ Eio.Process.spawn ~sw ~uid (Eio.Stdenv.process_mgr env) (prog :: args)
170170+ in
171171+ Atomic.set pid (Some (Eio.Process.pid p));
172172+ let status = Eio.Process.await p in
173173+ Atomic.set exit_status (Some status)
174174+ in
175175+ Out_channel.with_open_bin output @@ fun oc ->
176176+ let encoder = Jsonm.encoder ~minify:false (`Channel oc) in
177177+ let encode l =
178178+ Jsonm.encode encoder (`Lexeme l) |> function `Ok -> () | _ -> assert false
179179+ in
180180+ let finish () =
181181+ Jsonm.encode encoder `End |> function `Ok -> () | _ -> assert false
182182+ in
183183+ let () =
184184+ (* Header *)
185185+ match format with
186186+ | Csv -> Out_channel.output_string oc Open_event.csv_header
187187+ | Json -> encode `As
188188+ in
189189+ let callback event =
190190+ match Atomic.get pid with
191191+ | None -> 0
192192+ | Some pid ->
193193+ (if Int.equal (Open_event.get_pid event) pid then
194194+ match format with
195195+ | Csv ->
196196+ Out_channel.output_string oc (Open_event.to_csv_row event);
197197+ Out_channel.output_char oc '\n'
198198+ | Json ->
199199+ List.iter encode (json_to_lexemes (Open_event.to_json event)));
200200+ 0
201201+ in
202202+ let spawned = ref false in
203203+ let exit_callback exitting =
204204+ if not !spawned then (
205205+ Condition.broadcast start_process;
206206+ spawned := true);
207207+ Option.iter
208208+ (fun _ ->
209209+ exitting := false;
210210+ match format with
211211+ | Json ->
212212+ encode `Ae;
213213+ finish ()
214214+ | _ -> ())
215215+ (Atomic.get exit_status)
216216+ in
217217+ let bpf_callback = ringbuffer_polling_callback ~poll callback exit_callback in
218218+ run_ring_buffer bpf_callback
219219+220220+open Cmdliner
221221+open Cmdliner.Term.Syntax
222222+223223+let polling =
224224+ let doc = "The number of seconds to sleep between polls of the ringbuffer" in
225225+ Arg.(value & opt float 0.1 & info [ "p"; "poll" ] ~doc ~docv:"POLL")
226226+227227+let no_header =
228228+ let doc = "Disable printing the CSV header" in
229229+ Arg.(value & flag & info [ "no-header" ] ~doc)
230230+231231+let user =
232232+ let doc = "Username or UID to execute program as" in
233233+ Arg.(value & opt (some string) None & info [ "u"; "user" ] ~doc ~docv:"USER")
234234+235235+let format_conv : format Arg.conv =
236236+ let of_string s =
237237+ match String.lowercase_ascii s with
238238+ | "csv" -> Ok Csv
239239+ | "json" -> Ok Json
240240+ | _ -> Error (`Msg ("Unknown format: " ^ s))
241241+ in
242242+ let to_string = function Csv -> "csv" | Json -> "json" in
243243+ let pp ppf v = Fmt.string ppf (to_string v) in
244244+ Arg.conv ~docv:"FORMAT" (of_string, pp)
245245+246246+let format =
247247+ let doc = "Output format" in
248248+ Arg.(value & opt format_conv Csv & info [ "f"; "format" ] ~docv:"FORMAT" ~doc)
249249+250250+let output =
251251+ let doc =
252252+ "Output file for trace. Defaults to trace.<csv|json> depending on the \
253253+ $(format)."
254254+ in
255255+ Arg.(
256256+ value & opt (some string) None & info [ "o"; "output" ] ~docv:"OUTPUT" ~doc)
257257+258258+let all_cmd =
259259+ let doc = "Trace all open system calls" in
260260+ let man =
261261+ [
262262+ `P
263263+ "All calls to open will be traced and written to standard out in CSV \
264264+ format.";
265265+ ]
266266+ in
267267+ Cmd.v (Cmd.info ~doc ~man "all")
268268+ @@
269269+ let+ polling = polling and+ no_header = no_header in
270270+ all polling no_header
271271+272272+let exec_cmd =
273273+ let doc = "Execute a program and trace its open system calls" in
274274+ let man =
275275+ [
276276+ `P
277277+ "$(b,opentrace exec -- COMMAND) will execute COMMAND and trace only \
278278+ those open calls from that program.";
279279+ `P "Opentrace will include the children of the main process.";
280280+ ]
281281+ in
282282+ Cmd.v (Cmd.info ~doc ~man "exec")
283283+ @@
284284+ let+ prog =
285285+ Arg.(required & pos 0 (some string) None & Arg.info [] ~docv:"PROG")
286286+ and+ user = user
287287+ and+ format = format
288288+ and+ output = output
289289+ and+ args = Arg.(value & pos_right 0 string [] & Arg.info [] ~docv:"ARGS")
290290+ and+ poll = polling in
291291+ exec format output user poll (prog, args)
292292+293293+let opentrace_cmd =
294294+ let doc = "Trace all open system calls" in
295295+ let man =
296296+ [
297297+ `S Manpage.s_description;
298298+ `P "$(cmd) traces all open system calls";
299299+ `P
300300+ "$(cmd) can be used either to run an executable directly or to trace \
301301+ all open calls";
302302+ ]
303303+ in
304304+ let default = Term.(ret (const (`Help (`Auto, None)))) in
305305+ Cmd.group (Cmd.info ~doc ~man "opentrace") ~default [ all_cmd; exec_cmd ]
306306+307307+let main () = Cmd.eval opentrace_cmd
308308+let () = if !Sys.interactive then () else exit (main ())
+7-14
opentrace.opam
···11+# This file is generated by dune, edit dune-project instead
12opam-version: "2.0"
22-synopsis: "Trace the opening of files"
33-description: "A linux tool using eBPF for tracing calls to open files"
44-maintainer: ["Patrick Ferris <patrick@sirref.org>"]
55-authors: ["Patrick Ferris <patrick@sirref.org>"]
66-license: "MIT"
77-homepage: "https://tangled.sh/@patrick.sirref.org/opentrace"
33+synopsis: "Trace programs for the files they read and write"
44+description: ""
85depends: [
99- "dune" {>= "3.17"}
1010- "ocaml"
66+ "dune" {>= "3.14"}
77+ "ocaml" {>= "5.2.0"}
88+ "eio_main"
99+ "jsonm"
1110 "libbpf"
1211 "libbpf_maps"
1313- "ppx_blob"
1412 "odoc" {with-doc}
1513]
1614build: [
···2725 "@doc" {with-doc}
2826 ]
2927]
3030-homepage: "https://tangled.sh/@patrick.sirref.org/opentrace"
3131-pin-depends:[
3232- [ "libbpf.dev" "git+https://github.com/patricoferris/ocaml-libbpf#alpine" ]
3333- [ "libbpf_maps.dev" "git+https://github.com/patricoferris/ocaml-libbpf#alpine" ]
3434-]