···
4
+
type format = Csv | Json
let obj_path = "opentrace.bpf.o"
let obj_file = [%blob "opentrace.bpf.o"]
···
"tracepoint__syscalls__sys_enter_openat";
"tracepoint__syscalls__sys_enter_openat2";
11
-
(* "tracepoint__syscalls__sys_enter_open"; *)
13
+
"tracepoint__syscalls__sys_exit_openat";
14
+
"tracepoint__syscalls__sys_exit_openat2";
17
+
let json_to_lexemes json : Jsonm.lexeme list =
18
+
let rec loop acc = function
19
+
| `String _ as s -> s :: acc
20
+
| `Int i -> `Float (Int.to_float i) :: acc
24
+
(fun vacc (k, v) -> loop (`Name k :: vacc) v)
29
+
loop [] json |> List.rev
module Open_event = struct
···
let t : t structure typ = Ctypes.structure "event"
let ( -: ) ty label = Ctypes.field t label ty
let pid = uint32_t -: "e_pid"
51
+
let cgid = uint64_t -: "e_cgid"
52
+
let comm = array 16 char -: "e_comm"
let kind = int -: "e_kind"
let flags = int -: "e_flags"
let mode = uint32_t -: "e_mode"
let filename = array 256 char -: "e_filename"
57
+
let ret = int -: "e_ret"
let char_array_as_string a =
···
with Exit -> Buffer.contents b
let get_pid s = getf s pid |> Unsigned.UInt32.to_int
72
+
let get_cgid s = getf s cgid |> Unsigned.UInt64.to_int64
73
+
let get_comm s = getf s comm |> char_array_as_string
let get_flags s = getf s flags
let get_mode s = getf s mode |> Unsigned.UInt32.to_int
let get_fname s = getf s filename |> char_array_as_string
let get_kind s = getf s kind |> kind_of_int
78
+
let get_ret s = getf s ret
79
+
let csv_header = "pid,cgid,comm,kind,flags,mode,filename,return\n"
81
+
let to_csv_row event =
82
+
Format.sprintf "%i,%Ld,%S,%s,%i,%i,\"%s\",%i%!" (get_pid event)
83
+
(get_cgid event) (get_comm event)
84
+
(get_kind event |> kind_to_string)
85
+
(get_flags event) (get_mode event) (get_fname event) (get_ret event)
90
+
("pid", `Int (get_pid event));
91
+
("cgid", `Int (Int64.to_int @@ get_cgid event));
92
+
("comm", `String (get_comm event));
93
+
("kind", `String (get_kind event |> kind_to_string));
94
+
("flags", `Int (get_flags event));
95
+
("mode", `Int (get_mode event));
96
+
("fname", `String (get_fname event));
97
+
("ret", `Int (get_ret event));
101
+
let run_ring_buffer bpf_callback =
let dir = Filename.temp_dir "opentrace-" "" in
let full_obj_path = Filename.concat dir obj_path in
61
-
Out_channel.with_open_bin full_obj_path (fun oc -> Out_channel.output_string oc obj_file);
104
+
Out_channel.with_open_bin full_obj_path (fun oc ->
105
+
Out_channel.output_string oc obj_file);
106
+
with_bpf_object_open_load_link ~obj_path:full_obj_path ~program_names
109
+
let ringbuffer_polling_callback ~poll rb_cb exit_cb =
let bpf_callback obj _links =
(* Set signal handlers *)
let exitting = ref true in
···
Sys.(set_signal sigint sig_handler);
Sys.(set_signal sigterm sig_handler);
70
-
Format.printf "pid,kind,flags,mode,filename\n";
let map = Libbpf.bpf_object_find_map_by_name obj "rb" in
let callback : RingBuffer.callback =
let event = Ctypes.(!@(from_voidp Open_event.t data)) in
76
-
Format.printf "%i,%s,%i,%i,\"%s\"\n%!" (Open_event.get_pid event)
77
-
(Open_event.get_kind event |> Open_event.kind_to_string)
78
-
(Open_event.get_flags event)
79
-
(Open_event.get_mode event)
80
-
(Open_event.get_fname event);
RingBuffer.init map ~callback @@ fun rb ->
let _ : int = RingBuffer.poll rb ~timeout:1 in
90
-
with_bpf_object_open_load_link ~obj_path:full_obj_path ~program_names bpf_callback
132
+
let all poll no_header =
133
+
if no_header then () else Format.printf "%s" Open_event.csv_header;
134
+
let callback event =
135
+
Format.printf "%s\n%!" (Open_event.to_csv_row event);
138
+
let bpf_callback = ringbuffer_polling_callback ~poll callback (fun _ -> ()) in
139
+
run_ring_buffer bpf_callback
141
+
let exec format output user poll (prog, args) =
144
+
| Some file -> file
146
+
let ext = match format with Csv -> "csv" | Json -> "json" in
151
+
| None -> Unix.getenv "SUDO_UID" |> int_of_string
153
+
match int_of_string_opt user with
155
+
| None -> (Unix.getpwnam user).pw_uid)
158
+
let start_process = Condition.create () in
159
+
let mutex = Mutex.create () in
160
+
let pid = Atomic.make None in
161
+
let exit_status = Atomic.make None in
163
+
Domain.spawn @@ fun () ->
164
+
Eio_main.run @@ fun env ->
166
+
Condition.wait start_process mutex;
167
+
Eio.Switch.run @@ fun sw ->
169
+
Eio.Process.spawn ~sw ~uid (Eio.Stdenv.process_mgr env) (prog :: args)
171
+
Atomic.set pid (Some (Eio.Process.pid p));
172
+
let status = Eio.Process.await p in
173
+
Atomic.set exit_status (Some status)
175
+
Out_channel.with_open_bin output @@ fun oc ->
176
+
let encoder = Jsonm.encoder ~minify:false (`Channel oc) in
178
+
Jsonm.encode encoder (`Lexeme l) |> function `Ok -> () | _ -> assert false
181
+
Jsonm.encode encoder `End |> function `Ok -> () | _ -> assert false
186
+
| Csv -> Out_channel.output_string oc Open_event.csv_header
187
+
| Json -> encode `As
189
+
let callback event =
190
+
match Atomic.get pid with
193
+
(if Int.equal (Open_event.get_pid event) pid then
196
+
Out_channel.output_string oc (Open_event.to_csv_row event);
197
+
Out_channel.output_char oc '\n'
199
+
List.iter encode (json_to_lexemes (Open_event.to_json event)));
202
+
let spawned = ref false in
203
+
let exit_callback exitting =
204
+
if not !spawned then (
205
+
Condition.broadcast start_process;
215
+
(Atomic.get exit_status)
217
+
let bpf_callback = ringbuffer_polling_callback ~poll callback exit_callback in
218
+
run_ring_buffer bpf_callback
221
+
open Cmdliner.Term.Syntax
224
+
let doc = "The number of seconds to sleep between polls of the ringbuffer" in
225
+
Arg.(value & opt float 0.1 & info [ "p"; "poll" ] ~doc ~docv:"POLL")
228
+
let doc = "Disable printing the CSV header" in
229
+
Arg.(value & flag & info [ "no-header" ] ~doc)
232
+
let doc = "Username or UID to execute program as" in
233
+
Arg.(value & opt (some string) None & info [ "u"; "user" ] ~doc ~docv:"USER")
235
+
let format_conv : format Arg.conv =
237
+
match String.lowercase_ascii s with
239
+
| "json" -> Ok Json
240
+
| _ -> Error (`Msg ("Unknown format: " ^ s))
242
+
let to_string = function Csv -> "csv" | Json -> "json" in
243
+
let pp ppf v = Fmt.string ppf (to_string v) in
244
+
Arg.conv ~docv:"FORMAT" (of_string, pp)
247
+
let doc = "Output format" in
248
+
Arg.(value & opt format_conv Csv & info [ "f"; "format" ] ~docv:"FORMAT" ~doc)
252
+
"Output file for trace. Defaults to trace.<csv|json> depending on the \
256
+
value & opt (some string) None & info [ "o"; "output" ] ~docv:"OUTPUT" ~doc)
259
+
let doc = "Trace all open system calls" in
263
+
"All calls to open will be traced and written to standard out in CSV \
267
+
Cmd.v (Cmd.info ~doc ~man "all")
269
+
let+ polling = polling and+ no_header = no_header in
270
+
all polling no_header
273
+
let doc = "Execute a program and trace its open system calls" in
277
+
"$(b,opentrace exec -- COMMAND) will execute COMMAND and trace only \
278
+
those open calls from that program.";
279
+
`P "Opentrace will include the children of the main process.";
282
+
Cmd.v (Cmd.info ~doc ~man "exec")
285
+
Arg.(required & pos 0 (some string) None & Arg.info [] ~docv:"PROG")
287
+
and+ format = format
288
+
and+ output = output
289
+
and+ args = Arg.(value & pos_right 0 string [] & Arg.info [] ~docv:"ARGS")
290
+
and+ poll = polling in
291
+
exec format output user poll (prog, args)
293
+
let opentrace_cmd =
294
+
let doc = "Trace all open system calls" in
297
+
`S Manpage.s_description;
298
+
`P "$(cmd) traces all open system calls";
300
+
"$(cmd) can be used either to run an executable directly or to trace \
304
+
let default = Term.(ret (const (`Help (`Auto, None)))) in
305
+
Cmd.group (Cmd.info ~doc ~man "opentrace") ~default [ all_cmd; exec_cmd ]
307
+
let main () = Cmd.eval opentrace_cmd
308
+
let () = if !Sys.interactive then () else exit (main ())