Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/codex-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: codex-validate

on:
push:
branches:
- 'abhishek/validate-*'
workflow_dispatch:

jobs:
validate:
runs-on: ubuntu-latest
container:
image: alpine:latest
steps:
- name: Install apk packages
run: |
apk add autoconf bash bubblewrap build-base clang coreutils git libstdc++ \
libstdc++-dev libunwind-static linux-headers lld llvm20 llvm20-dev llvm20-static \
nodejs opam rsync upx zlib-static zstd-static
echo "PATH=/usr/lib/llvm20/bin:$PATH" >> "$GITHUB_ENV"

- name: Checkout code
uses: actions/checkout@v4

- run: git config --global --add safe.directory "$(pwd)"

- uses: actions/cache@v4
with:
path: ~/.opam
key: codex-opam-5.2.0-ox-231c88c2e564fdca40e15e750aacad5fb0887435-1

- name: Use OCaml 5.2.0+ox
run: |
export OPAMYES=1
export OPAMJOBS=$(($(nproc) + 2))
export OPAMROOTISOK=1
echo "OPAMYES=1" >> "$GITHUB_ENV"
echo "OPAMJOBS=$OPAMJOBS" >> "$GITHUB_ENV"
echo "OPAMROOTISOK=$OPAMROOTISOK" >> "$GITHUB_ENV"

opam init --bare -yav https://github.com/ocaml/opam-repository.git
opam switch set 5.2.0+ox 2>/dev/null || \
opam switch create 5.2.0+ox --repos "ox=git+https://github.com/oxcaml/opam-repository.git#231c88c2e564fdca40e15e750aacad5fb0887435,default"

- run: opam install . --deps-only --locked --with-test
- run: opam exec -- dune build @fmt
- run: opam exec -- make PROFILE=static
- run: opam exec -- dune runtest --profile=static
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ You can point magic-trace at a function such that when your application calls it

Congratulations, you just magically traced your first program!

In contrast to traditional `perf` workflows, magic-trace excels at hypothesis generation. For example, you might notice that taking 6us to run `cos` is a really long time! If you zoom in even more, you'll see that there's actually five pink "\[untraced\]" cells in there. If you re-run magic-trace with root and pass it `-trace-include-kernel`, you'll see stacktraces for those. They're page fault handlers! The demo program actually calls `cos` twice. If you zoom in even more near the end of the 6us `cos` call, you'll see that the second call takes *far* less time and does not page fault.
In contrast to traditional `perf` workflows, magic-trace excels at hypothesis generation. For example, you might notice that taking 6us to run `cos` is a really long time! If you zoom in even more, you'll see that there's actually five pink "\[untraced\]" cells in there. If you re-run magic-trace with kernel tracing permissions and pass it `-trace-include-kernel`, you'll see stacktraces for those. They're page fault handlers! The demo program actually calls `cos` twice. If you zoom in even more near the end of the 6us `cos` call, you'll see that the second call takes *far* less time and does not page fault.

# How to use it

Expand Down
121 changes: 104 additions & 17 deletions src/perf_capabilities.ml
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,63 @@ let supports_last_branch_record () =
List.fold flags ~init:true ~f:(fun acc flags -> acc && contains_pdcm flags)
;;

let supports_tracing_kernel () =
(* Only allow tracing the kernel if we are root. `perf` will start even without this,
but the generated traces will be broken, so disallow it here.

This check is technically stricter than it has to be. We could query the capability
bits of the perf binary here instead, as per
<https://perf.wiki.kernel.org/index.php/Perf_tools_support_for_Intel%C2%AE_Processor_Trace#Adding_capabilities_to_perf> *)
Int.(Core_unix.geteuid () = 0)
let capability_grants_effective_capability capability_group capability =
match String.lsplit2 capability_group ~on:'=' with
| None -> false
| Some (capabilities, permitted_sets) ->
String.exists permitted_sets ~f:(Char.equal 'e')
&& capabilities |> String.split ~on:',' |> List.exists ~f:(String.equal capability)
;;

let getcap_output_grants_kernel_tracing getcap_output =
getcap_output
|> String.split_lines
|> List.exists ~f:(fun line ->
match String.split line ~on:' ' |> List.filter ~f:(Fn.non String.is_empty) with
| [] | [ _ ] -> false
| _path :: capability_groups ->
List.exists capability_groups ~f:(fun capability_group ->
capability_grants_effective_capability capability_group "cap_perfmon"
|| capability_grants_effective_capability capability_group "cap_sys_admin"))
;;

let resolve_executable_from_path executable =
if String.contains executable '/'
then Some executable
else (
match Sys.getenv "PATH" with
| None -> None
| Some path ->
path
|> String.split ~on:':'
|> List.find_map ~f:(fun dir ->
let candidate = dir ^/ executable in
match Sys_unix.file_exists candidate with
| `Yes -> Some candidate
| `No | `Unknown -> None))
;;

let perf_has_kernel_tracing_capability ~perf_path =
match resolve_executable_from_path perf_path with
| None -> return false
| Some perf_path ->
(match%bind
Monitor.try_with (fun () ->
Process.create_exn ~prog:"getcap" ~args:[ perf_path ] ())
with
| Error _ -> return false
| Ok getcap_proc ->
let%map { stdout; _ } = Process.collect_output_and_wait getcap_proc in
getcap_output_grants_kernel_tracing stdout)
;;

let supports_tracing_kernel ~perf_path =
(* `perf` can trace the kernel as root, or when the perf executable has suitable
effective file capabilities. If [getcap] is unavailable, keep the historical
root-only behavior. *)
if Int.(Core_unix.geteuid () = 0)
then return true
else perf_has_kernel_tracing_capability ~perf_path
;;

let kernel_version_at_least ~major ~minor version =
Expand All @@ -108,15 +157,53 @@ let detect_exn () =
let%bind perf_version_proc =
Process.create_exn ~prog:Env_vars.perf_path ~args:[ "--version" ] ()
in
let%map { stdout; _ } = Process.collect_output_and_wait perf_version_proc in
let%bind { stdout; _ } = Process.collect_output_and_wait perf_version_proc in
let version = Version.of_perf_version_string_exn stdout in
let%bind supports_tracing_kernel =
supports_tracing_kernel ~perf_path:Env_vars.perf_path
in
let set_if bool flag cap = cap + if bool then flag else empty in
empty
|> set_if (supports_configurable_psb_period ()) configurable_psb_period
|> set_if (supports_tracing_kernel ()) kernel_tracing
|> set_if (supports_kcore version) kcore
|> set_if (supports_snapshot_on_exit version) snapshot_on_exit
|> set_if (supports_last_branch_record ()) last_branch_record
|> set_if (supports_dlfilter version) dlfilter
|> set_if (supports_ctlfd version) ctlfd
return
(empty
|> set_if (supports_configurable_psb_period ()) configurable_psb_period
|> set_if supports_tracing_kernel kernel_tracing
|> set_if (supports_kcore version) kcore
|> set_if (supports_snapshot_on_exit version) snapshot_on_exit
|> set_if (supports_last_branch_record ()) last_branch_record
|> set_if (supports_dlfilter version) dlfilter
|> set_if (supports_ctlfd version) ctlfd)
;;

let%expect_test "getcap output grants kernel tracing via effective cap_perfmon" =
let output = "/usr/bin/perf cap_sys_ptrace,cap_syslog,cap_perfmon=ep\n" in
print_s [%sexp (getcap_output_grants_kernel_tracing output : bool)];
[%expect {| true |}];
return ()
;;

let%expect_test "getcap output grants kernel tracing via effective cap_sys_admin" =
let output = "/usr/bin/perf cap_sys_admin=ep\n" in
print_s [%sexp (getcap_output_grants_kernel_tracing output : bool)];
[%expect {| true |}];
return ()
;;

let%expect_test "getcap output requires effective capabilities" =
let output = "/usr/bin/perf cap_perfmon=p\n" in
print_s [%sexp (getcap_output_grants_kernel_tracing output : bool)];
[%expect {| false |}];
return ()
;;

let%expect_test "getcap output ignores unrelated capabilities" =
let output = "/usr/bin/perf cap_sys_ptrace,cap_syslog=ep\n" in
print_s [%sexp (getcap_output_grants_kernel_tracing output : bool)];
[%expect {| false |}];
return ()
;;

let%expect_test "executable resolution keeps explicit paths" =
print_s [%sexp (resolve_executable_from_path "/usr/bin/perf" : string option)];
[%expect {| (/usr/bin/perf) |}];
return ()
;;
3 changes: 2 additions & 1 deletion src/perf_tool_backend.ml
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ module Recording = struct
if not Env_vars.perf_is_privileged
then
Deferred.Or_error.error_string
"magic-trace must be run as root in order to trace the kernel"
"magic-trace needs permission to trace the kernel. Run as root, or give the \
perf executable effective cap_perfmon or cap_sys_admin file capabilities."
else return (Ok ())
in
(match when_to_snapshot, subcommand with
Expand Down