Opentrace and Supervisions [weekly-2025-05-26]
- May 26, 2025
- Patrick Ferris
Over the past two weeks I have mainly split my time (amongst many things) developing opentrace and doing revision supervisions.
Opentrace [open-trace]
- May 19, 2025
- Patrick Ferris
- https://tangled.sh/@patrick.sirref.org/opentrace
Thanks to Koonwen's excellent libbpf bindings in OCaml, I have been building a little tool called opentrace to make it easier to track an executable's inputs and outputs.
This work was inspired my Michael's self-proclaimed "gross hack": pyshark. Whilst pyshark achieves its goals by injecting code into commonly used python objects and methods, opentrace uses eBPF. By using a lower-level API (hooks in the kernel), opentrace can remain programming language agnostic. However, less information is none about the user's intent compared to something like pyshark.
Monitoring the System
- May 19, 2025
- Patrick Ferris
opentrace has an all command that will trace the entire system.
$ sudo opentrace all --flags=O_WRONLY pid,cgid,comm,kind,flags,mode,filename,return 16324,4417,".nheko-wrapped",openat,577,438,"/home/patrick/.cache/nheko/nheko/curl_alt_svc_cache.txt",47 16324,4417,".nheko-wrapped",openat,193,33188,"/home/patrick/.cache/nheko/nheko/cr3ZUHqBErIOe3PlwJ1SuU8zFKKxL12VzrRoHYMH.tmp",47 16324,4417,".nheko-wrapped",openat,577,438,"/home/patrick/.cache/nheko/nheko/curl_alt_svc_cache.txt",47 16324,4417,".nheko-wrapped",openat,193,33188,"/home/patrick/.cache/nheko/nheko/QfZTdZSuC56NdcDQ3aMxJc3BhMhAj8PmtYW1zFDP.tmp",47 2530,4235,"systemd",openat,524865,438,"/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/gammastep.service/cgroup.subtree_control",41 2530,4235,"systemd",openat,524545,0,"/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/gammastep.service/memory.min",41 2530,4235,"systemd",openat,524545,0,"/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/gammastep.service/memory.low",41 2530,4235,"systemd",openat,524545,0,"/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/gammastep.service/memory.high",41 2530,4235,"systemd",openat,524545,0,"/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/gammastep.service/memory.max",41
The --flags=O_WRONLY argument filters the events where the O_WRONLY flag was set in the call to open.
We also get the name of the current executable linked to the task (comm). The -wrapped is an artefact of using Nix.
Tracing an Executable
- May 19, 2025
- Patrick Ferris
The primary use case for this tool is to inspect what files your program might be reading and writing.
$ sudo opentrace exec --format=json --flags=O_CREAT -- opam list $ cat trace.json | jq ".[] | .fname" "/home/patrick/.opam/cshell/.opam-switch/packages/cache" "/home/patrick/.opam/log/log-118747-29da3d.out" "/home/patrick/.opam/log/log-118747-29da3d.err" "/home/patrick/.opam/log/log-118747-29da3d.env" "/home/patrick/.opam/log/log-118747-29da3d.info"
The "flags" argument can specify a small boolean formula for checking the open flags of a particular event with | (or), & (and), and ~ (not). Parentheses can be used for precedence.
$ sudo opentrace exec --flags="O_WRONLY|O_RDONLY" -- ocaml --version
Spawning Subprocesses
- May 19, 2025
- Patrick Ferris
One feature opentrace needs (in this proof-of-concept phase) is the ability to also trace subprocesses.
opentrace is primarily an eBPF program that is loaded into the kernel and communicates with an OCaml program. Events are communicated via a ring buffer and most of the post-processing happens in OCaml. To capture subprocesses, opentrace creates a new control group (cgroup) and places the new process into that group. This gives opentrace a new identifier to track, namely the cgroup.
So consider the following program.
let () =
Eio_posix.run @@ fun env ->
Eio.Path.(save ~create:(`Or_truncate 0o664) (env#fs / "hello.txt") "hello");
Eio.Process.run env#process_mgr
[ "/bin/bash"; "-c"; "echo 'heya' > heya.txt" ]
It first creates a file using direct calls to functions like openat. Then it spawns a process which creates a new file called heya.txt. This happens in a separate process. However, with the --cgroups flag we can capture both interactions with the operating system.
$ sudo opentrace exec --cgroups --flags="O_CREAT|O_TRUNC" ./main.exe pid,cgid,comm,kind,flags,mode,filename,return 153187,530807,"main.exe",openat,526914,436,"hello.txt",5 153192,530807,"bash",openat,577,438,"heya.txt",3
Eio's Process API
- May 19, 2025
- Patrick Ferris
I have used the Eio_unix fork action process API to be able to extend what happens in the child process. Loading most eBPF programs into the kernel requires special privileges hence the need for sudo. When a user requests for a particular program to be executed and traced, opentrace spawns a process via the Eio Process API. Opentrace defines a few new so-called "fork actions", little fragments of C code that are run after the call to fork (clone). Most likely this ends with a call to execve, but other calls are possible for example setuid allowing opentrace to change the user of the child process so it does not run as root. Similarly, this is where (if used) we create the cgroup and place the process into that group.
Limitations: Io_uring
- May 19, 2025
- Patrick Ferris
Whilst testing opentrace against some of the tools I use nearly daily, I noticed some events were being missed. I tried tracing forester, and only the initial read of forest.toml was logged. It dawned on me that the reason for this was that forester (via eio) was using io_uring to perform most of the IO. Most attempts to open files were bypassing the open system calls, and instead they were being performed by the kernel after reading a submission request for an openat2-style call!
This is not news to seasoned, Linux systems programmers. Io_uring bypasses SECCOMP filters for exactly the same reasons.
$ sudo opentrace exec -- forester build $ cat trace.csv pid,cgid,comm,kind,flags,mode,filename,return 155007,535570,"forester",openat,524288,0,"forest.toml",5 155007,535570,"forester",Uring,2621440,0,"",0 155021,535570,"cp",openat,131072,0,"/home/patrick/documents/forest/theme/favicon-32x32.png",4 155007,535570,"forester",Uring,2686976,0,"",0 155007,535570,"iou-wrk-155007",Uring,557634,420,"",0 155007,535570,"iou-wrk-155007",Uring,557634,420,"",0 155007,535570,"iou-wrk-155007",Uring,557634,420,"",0
It is interesting to note two things here:
We can tell that forester reads the configuration file probably using something like
In_channelin OCaml (it does).It appears that Uring is performing IO in both worker threads and directly.
The file paths are empty at the moment as I cannot find a clean way to trace openat submission requests into Uring without it sometimes going very wrong. I have tried quite a few methods (e.g. tracing do_filp_open) and at the moment I am tracing io_openat2, but this seems quite brittle, and often the filename is completely garbled, so I do not set it. If anyone has any ideas to trace Io_uring more reliably, I am all ears!
Supervisions
- May 26, 2025
- Patrick Ferris
The first year students I teach will be doing their first year exams soon. I have been helping them revise for foundations of computer science and discrete mathematics. Whilst doing so, I have been thinking a good deal about Jon Sterling's post about assessment bureaucracy. In particular:
At moments like this, it is a good idea to pause and reflect on whether it is better for our students that each faculty member spend a cumulative two months doing literally nothing but assessment and higher-order practices related to assessment, vs. other activities that could benefit our students more (including actual teaching, of which we do astonishingly little at Cambridge).