diff --git a/.github/workflows/codex-validate.yml b/.github/workflows/codex-validate.yml new file mode 100644 index 000000000..bc31b7f86 --- /dev/null +++ b/.github/workflows/codex-validate.yml @@ -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 diff --git a/README.md b/README.md index 832a55e44..a992f61b6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/perf_capabilities.ml b/src/perf_capabilities.ml index 2597d756b..d5318b10d 100644 --- a/src/perf_capabilities.ml +++ b/src/perf_capabilities.ml @@ -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 - *) - 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 = @@ -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 () ;; diff --git a/src/perf_tool_backend.ml b/src/perf_tool_backend.ml index 68470a870..2d701b9ac 100644 --- a/src/perf_tool_backend.ml +++ b/src/perf_tool_backend.ml @@ -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