Skip to content

Perf capability check #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
216 changes: 216 additions & 0 deletions src/capability.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
(** Stand in for C capability library; suggested to use that library once it is open source *)

open Core

module Capability = struct
type t =
{ effective : bool
; permitted : bool
; inherited : bool
}
[@@deriving fields]

let create = Fields.create
let empty = create ~effective:false ~permitted:false ~inherited:false
end

(* doesn't work if file has spaces but this is just for perf & it's good enough *)
let split_init_rest_capabilities (input_text : string) =
match String.split input_text ~on:' ' with
| [] | [ _ ] -> raise_s [%message "mal-formatted string" (input_text : string)]
| (_ : Filename.t) :: maybe_init :: rest ->
if String.is_prefix maybe_init ~prefix:"="
|| String.is_prefix maybe_init ~prefix:"all="
then (
let all_caps =
let eff = String.contains maybe_init 'e' in
let per = String.contains maybe_init 'p' in
let inh = String.contains maybe_init 'i' in
Capability.create ~effective:eff ~permitted:per ~inherited:inh
in
all_caps, rest)
else (
(* maybe_init is first capability *)
let all_caps = Capability.empty in
all_caps, maybe_init :: rest)
;;

(* override is not a valid file capability; it is an indicator of which capability bits to alter based on the operator *)
let compute_capability (override : Capability.t) (default : Capability.t) operator =
if Char.equal operator '='
then override
else (
let add_or_drop =
match operator with
| '+' -> true
| '-' -> false
| _ -> failwith "invalid symbol"
in
Capability.create
~effective:(if override.effective then add_or_drop else default.effective)
~permitted:(if override.permitted then add_or_drop else default.permitted)
~inherited:(if override.inherited then add_or_drop else default.inherited))
;;

let get_final_cap (all_caps : Capability.t) indication =
if String.equal indication ""
then all_caps
else (
let operator = String.get indication 0 in
let letters = String.drop_prefix indication 1 in
let eff = String.contains letters 'e' in
let per = String.contains letters 'p' in
let inh = String.contains letters 'i' in
let indicated_capability =
Capability.create ~effective:eff ~permitted:per ~inherited:inh
in
compute_capability indicated_capability all_caps operator)
;;

let split_indication_from_cap_string clause =
let operators = [ '+'; '-'; '=' ] in
let indication =
String.lstrip clause ~drop:(fun c -> not (List.mem operators c ~equal:Char.equal))
in
let cap_string = String.chop_suffix_exn clause ~suffix:indication in
indication, cap_string
;;

type t =
{ all_caps : Capability.t
; caps_by_tag : Capability.t String.Map.t
}

let process_capabilities input_text : t =
let (all_caps : Capability.t), assignment_sets =
split_init_rest_capabilities input_text
in
let caps_by_tag =
List.concat_map assignment_sets ~f:(fun assign_set ->
let cap_clauses = String.split assign_set ~on:',' |> List.rev in
let indication, indicator_cap_string =
let indicator_clause = List.hd_exn cap_clauses in
split_indication_from_cap_string indicator_clause
in
let cap_strings = List.tl_exn cap_clauses in
let final_capability : Capability.t = get_final_cap all_caps indication in
List.map (indicator_cap_string :: cap_strings) ~f:(fun cap_string ->
cap_string, final_capability))
|> String.Map.of_alist_exn
in
{ all_caps; caps_by_tag }
;;

let contains_cap t cap_string =
List.mem (Map.keys t.caps_by_tag) cap_string ~equal:String.equal
;;

let get_cap_sets t cap_string =
if contains_cap t cap_string
then Map.find t.caps_by_tag cap_string |> Option.value ~default:t.all_caps
else Capability.empty
;;

let has_eff t cap_string = get_cap_sets t cap_string |> Capability.effective
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let has_eff t cap_string = get_cap_sets t cap_string |> Capability.effective
let has_effective t cap_string = get_cap_sets t cap_string |> Capability.effective

Nit: avoid shortened function/variable names for readability.


let linux_version_has_cap_perfmon linux_version =
let split_version = String.split linux_version ~on:'.' in
let head = Int.of_string (List.hd_exn split_version) in
(Int.equal head 5 && Int.of_string (List.nth_exn split_version 1) >= 8) || head > 5
;;

let does_not_remove_effective t cap_string =
if contains_cap t cap_string then has_eff t cap_string else true
;;

let check_perf_support getcap linux_version =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function should probably live in perf_capabilities.ml, within supports_tracing_kernel.

let stripped_getcap = String.strip getcap in
if String.equal stripped_getcap ""
then false
else if not (String.contains stripped_getcap ' ')
then
failwith
"Invalid capability string" (* must not have capability if contains no spaces *)
else (
let caps = process_capabilities getcap in
if Capability.effective caps.all_caps (* if all_caps has eff *)
then
if linux_version_has_cap_perfmon linux_version
then
if does_not_remove_effective caps "cap_perfmon"
then true
else does_not_remove_effective caps "cap_sys_admin"
else does_not_remove_effective caps "cap_sys_admin"
else has_eff caps "cap_perfmon" || has_eff caps "cap_sys_admin")
;;

let run_test test_string linux_version =
printf "%s" (Bool.to_string (check_perf_support test_string linux_version))
;;

let%test_module "perf_support_tests_1" =
(module struct
let%expect_test "only has cap_perfmon" =
run_test "/usr/bin/perf cap_perfmon=ep" "5.10.118-111.515.amzn2.x86_64";
[%expect {|true|}]
;;

let%expect_test "has caps" =
run_test
"/usr/bin/perf cap_sys_admin,cap_perfmon=ep"
"5.10.118-111.515.amzn2.x86_64";
[%expect {|true|}]
;;

let%expect_test "has cap with others, two sets" =
run_test
"/usr/bin/perf all=ep cap_a,cap_b-p cap_perfmon,cap_d+i"
"5.10.118-111.515.amzn2.x86_64";
[%expect {|true|}]
;;

let%expect_test "all wrong caps" =
run_test
"/usr/bin/perf all=ep cap_a,cap_b-p cap_c,cap_d+i cap_sys_admin-ep"
"5.7.118-111.515.amzn2.x86_64";
[%expect {|false|}]
;;

let%expect_test "only has cap_sys_admin" =
run_test "/usr/bin/perf cap_sys_admin=ep" "5.10.118-111.515.amzn2.x86_64";
[%expect {|true|}]
;;

let%expect_test "similar name" =
run_test "/usr/bin/perf not_cap_perfmon=ep" "5.10.118-111.515.amzn2.x86_64";
[%expect {|false|}]
;;

let%expect_test "right cap, no permissions" =
run_test "/usr/bin/perf cap_sys_admin" "5.10.118-111.515.amzn2.x86_64";
[%expect {|false|}]
;;

let%expect_test "right cap, but only per" =
run_test "/usr/bin/perf cap_sys_admin=p" "5.10.118-111.515.amzn2.x86_64";
[%expect {|false|}]
;;

let%expect_test "subtract from default to be no permission" =
run_test "/usr/bin/perf =ep cap_sys_admin-ep" "5.7.118-111.515.amzn2.x86_64";
[%expect {|false|}]
;;

let%expect_test "cap_sys_admin is subtracted but cap_perfmon remains" =
run_test
"/usr/bin/perf =ep cap_sys_admin-ep cap_perfmon"
"5.10.118-111.515.amzn2.x86_64";
[%expect {|true|}]
;;

let%expect_test "new version can have implicit cap_perfmon" =
run_test "/usr/bin/perf =ep cap_sys_admin-ep" "5.10.118-111.515.amzn2.x86_64";
[%expect {|true|}]
;;
end)
;;
1 change: 1 addition & 0 deletions src/capability.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
val check_perf_support : string -> string -> bool
27 changes: 18 additions & 9 deletions src/perf_capabilities.ml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
open! Core
open! Async
open! Unix

let bit n = Int63.of_int (1 lsl n)
let configurable_psb_period = bit 0
Expand Down Expand Up @@ -77,13 +78,20 @@ let supports_last_branch_record () =
;;

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)
if Int.(Core_unix.geteuid () = 0)
then Deferred.return true
else (
(* get capability string *)
let%bind perf_path = Process.create_exn ~prog:"which" ~args:[ "perf" ] () in
let%bind path_string_slashn = Reader.contents (Process.stdout perf_path) in
let path_string = String.chop_suffix_exn path_string_slashn ~suffix:"\n" in
let%bind linux_version = Process.create_exn ~prog:"uname" ~args:[ "-r" ] () in
let%bind version_string_slashn = Reader.contents (Process.stdout linux_version) in
let version_string = String.chop_suffix_exn version_string_slashn ~suffix:"\n" in
let%bind capabilities = Process.create_exn ~prog:"getcap" ~args:[ path_string ] () in
let%map cap_string = Reader.contents (Process.stdout capabilities) in
(* parse string and check for kernel tracing capabilities *)
Capability.check_perf_support cap_string version_string)
;;

let kernel_version_at_least ~major ~minor version =
Expand All @@ -101,12 +109,13 @@ let supports_dlfilter = kernel_version_at_least ~major:5 ~minor:14

let detect_exn () =
let%bind perf_version_proc = Process.create_exn ~prog:"perf" ~args:[ "--version" ] () in
let%map version_string = Reader.contents (Process.stdout perf_version_proc) in
let%bind version_string = Reader.contents (Process.stdout perf_version_proc) in
let%map supports_tracing_kernel_output = supports_tracing_kernel () in
let version = Version.of_perf_version_string_exn version_string 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_tracing_kernel_output 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
Expand Down