cli/bpfcov.c (1,228 lines of code) (raw):

#define _GNU_SOURCE #ifdef NDEBUG #define DEBUG 0 #else #define DEBUG 1 #endif /* C standard library */ #include <assert.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <time.h> #include <string.h> /* POSIX */ #include <unistd.h> #include <fcntl.h> #include <sys/user.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/vfs.h> /* Linux */ #include <syscall.h> #include <sys/ptrace.h> #include <linux/limits.h> #include <linux/magic.h> #include <bpf/bpf.h> #include <argp.h> // -------------------------------------------------------------------------------------------------------------------- // Global info // -------------------------------------------------------------------------------------------------------------------- #define TOOL_NAME "bpfcov" const char *argp_program_version = TOOL_NAME " 0.1"; const char *argp_program_bug_address = "leo"; error_t argp_err_exit_status = 1; // -------------------------------------------------------------------------------------------------------------------- // Prototypes // -------------------------------------------------------------------------------------------------------------------- const char *argp_key(int key, char *str); struct root_args; typedef int (*callback_t)(struct root_args *args); static error_t root_parse(int key, char *arg, struct argp_state *state); void root(int argc, char **argv); void run_cmd(struct argp_state *state); static error_t run_parse(int key, char *arg, struct argp_state *state); int run(struct root_args *args); void gen_cmd(struct argp_state *state); static error_t gen_parse(int key, char *arg, struct argp_state *state); int gen(struct root_args *args); void out_cmd(struct argp_state *state); static error_t out_parse(int key, char *arg, struct argp_state *state); int out(struct root_args *args); static bool is_bpffs(char *bpffs_path); static void strip_trailing_char(char *str, char c); static void replace_with(char *str, const char what, const char with); static void strip_extension(char *str); static void handle_map_pins(struct root_args *args, struct argp_state *state, bool unpin); static void wait_or_exit(struct root_args *args, pid_t pid, char *err); // -------------------------------------------------------------------------------------------------------------------- // Logging // -------------------------------------------------------------------------------------------------------------------- void print_log(int level, const char *prefix, struct root_args *args, const char *fmt, ...); #define log_erro(args, fmt, ...) \ do \ { \ if (DEBUG) \ print_log(0, TOOL_NAME ": %s: ", args, fmt, __VA_ARGS__); \ } while (0) #define log_warn(args, fmt, ...) \ do \ { \ if (DEBUG) \ print_log(1, TOOL_NAME ": %s: ", args, fmt, __VA_ARGS__); \ } while (0) #define log_info(args, fmt, ...) \ do \ { \ if (DEBUG) \ print_log(2, TOOL_NAME ": %s: ", args, fmt, __VA_ARGS__); \ } while (0) #define log_debu(args, fmt, ...) \ do \ { \ if (DEBUG) \ print_log(3, TOOL_NAME ": %s: ", args, fmt, __VA_ARGS__); \ } while (0) #define log_fata(args, fmt, ...) \ log_erro(args, fmt, __VA_ARGS__); \ exit(EXIT_FAILURE); // -------------------------------------------------------------------------------------------------------------------- // Entrypoint // -------------------------------------------------------------------------------------------------------------------- int main(int argc, char **argv) { root(argc, argv); return 0; } // -------------------------------------------------------------------------------------------------------------------- // CLI / bpfcov // -------------------------------------------------------------------------------------------------------------------- #define NUM_PINNED_MAPS 4 #define FOREACH_FORMAT(FORMAT) \ FORMAT(FORMAT_, html) \ FORMAT(FORMAT_, json) \ FORMAT(FORMAT_, lcov) #define GEN_ENUM(PREFIX, ENUM) PREFIX##ENUM, #define GEN_STRING(PREFIX, STRING) #STRING, enum out_format { FOREACH_FORMAT(GEN_ENUM) }; static const char *format_string[] = {FOREACH_FORMAT(GEN_STRING)}; typedef enum out_format out_format_t; struct root_args { bool unpin; char *output; char *bpffs; char *cov_root; char *prog_root; char *pin[NUM_PINNED_MAPS]; char **profraw; char *report_path; int num_profraw; out_format_t out_format; int verbosity; callback_t command; char **program; }; const char ROOT_BPFFS_OPT_KEY = 0x80; const char ROOT_BPFFS_OPT_LONG[] = "bpffs"; const char ROOT_BPFFS_OPT_ARG[] = "path"; const char ROOT_VERBOSITY_OPT_KEY = 'v'; const char ROOT_VERBOSITY_OPT_LONG[] = "verbose"; const char ROOT_VERBOSITY_OPT_ARG[] = "level"; static struct argp_option root_opts[] = { {"OPTIONS:", 0, 0, OPTION_DOC, 0, 0}, {ROOT_BPFFS_OPT_LONG, ROOT_BPFFS_OPT_KEY, ROOT_BPFFS_OPT_ARG, 0, "Set the BPF FS path (defaults to /sys/fs/bpf)", 1}, { ROOT_VERBOSITY_OPT_LONG, ROOT_VERBOSITY_OPT_KEY, ROOT_VERBOSITY_OPT_ARG, OPTION_ARG_OPTIONAL, "Set the verbosity level when not built for release (defaults to 0)", 1, }, {"\n", 0, 0, OPTION_DOC, 0, 0}, {"GLOBALS:", 0, 0, OPTION_DOC, 0, 0}, {0} // . }; static char root_docs[] = "\n" "Obtain source-based code coverage from your instrumented eBPF applications." "\v" " EXAMPLES:\n" " bpfcov run <program>\n" " bpfcov gen <program>\n" " bpfcov out <program.profraw>+\n"; static struct argp root_argp = { .options = root_opts, .parser = root_parse, .args_doc = "[run|gen|out] <arg(s)>", .doc = root_docs, }; static error_t root_parse(int key, char *arg, struct argp_state *state) { struct root_args *args = state->input; char str[2]; log_debu(args, "parsing <root> %s = '%s'\n", argp_key(key, str), arg ? arg : "(null)"); switch (key) { // Initialization case ARGP_KEY_INIT: args->bpffs = "/sys/fs/bpf"; // args->verbosity = 0; // It needs to be set before the parsing starts args->command = NULL; args->program = calloc(PATH_MAX, sizeof(char *)); args->output = NULL; args->unpin = false; break; case ROOT_BPFFS_OPT_KEY: if (strlen(arg) > 0) { strip_trailing_char(arg, '/'); args->bpffs = arg; break; } argp_error(state, "option '--%s' requires a %s", ROOT_BPFFS_OPT_LONG, ROOT_BPFFS_OPT_ARG); break; case ROOT_VERBOSITY_OPT_KEY: if (arg) { errno = 0; char *end; long num = strtol(arg, &end, 10); if (end == arg) { argp_error(state, "option '--%s' requires a numeric %s", ROOT_VERBOSITY_OPT_LONG, ROOT_VERBOSITY_OPT_ARG); } if (num < 0 || num > 3) { argp_error(state, "option '--%s' requires a %s value in [0,3]", ROOT_VERBOSITY_OPT_LONG, ROOT_VERBOSITY_OPT_ARG); } args->verbosity = (int)num; } else { args->verbosity++; } break; case ARGP_KEY_ARG: assert(arg); /**/ if (strncmp(arg, "run", 3) == 0) { args->command = &run; run_cmd(state); } else if (strncmp(arg, "gen", 3) == 0) { args->command = &gen; gen_cmd(state); } else if (strncmp(arg, "out", 3) == 0) { args->command = &out; out_cmd(state); } else { args->program[state->arg_num] = arg; } break; // Args validation case ARGP_KEY_END: if (state->arg_num == 0) { argp_state_help(state, state->err_stream, ARGP_HELP_STD_HELP); } if (args->command != &out && args->program[0] == NULL) { // This should never happen argp_error(state, "unexpected missing <program>"); } break; // Final validations, checks, and settings case ARGP_KEY_FINI: bool is_run = args->command == &run; // When the subcommand is <out> // - do not validate BPF FS // - do not generate pinning paths // - do not clean up (<run>) or check (<gen>) pinned maps if (args->command == &out) { break; } // Check the BPF filesystem if (!is_bpffs(args->bpffs)) { argp_error(state, "the BPF filesystem is not mounted at %s", args->bpffs); } // Create the coverage directory in the BPF filesystem char cov_root[PATH_MAX]; int cov_root_len = snprintf(cov_root, PATH_MAX, "%s/%s", args->bpffs, "cov"); if (cov_root_len >= PATH_MAX) { argp_error(state, "coverage root path too long"); } if (is_run && mkdir(cov_root, 0700) && errno != EEXIST) { argp_error(state, "could not create '%s'", cov_root); } args->cov_root = cov_root; // Obtain the program name and create a directory in the BPF filesystem for it char *prog_name = basename(args->program[0]); char prog_root[PATH_MAX]; int prog_root_len = snprintf(prog_root, PATH_MAX, "%s/%s", cov_root, prog_name); if (prog_root_len >= PATH_MAX) { argp_error(state, "program root path too long"); } char *prog_root_sane = strdup(prog_root); replace_with(prog_root_sane, '.', '_'); // Sanitize because BPF FS doesn't accept dots if (is_run && mkdir(prog_root_sane, 0700) && errno != EEXIST) { argp_error(state, "could not create '%s'", prog_root_sane); } args->prog_root = prog_root_sane; log_info(args, "root directory for map pins at '%s'\n", prog_root_sane); // Create pinning path for the counters map char pin_profc[PATH_MAX]; int pin_profc_len = snprintf(pin_profc, PATH_MAX, "%s/%s", prog_root_sane, "profc"); if (pin_profc_len >= PATH_MAX) { argp_error(state, "counters pinning path too long"); } args->pin[0] = pin_profc; // Create pinning path for the data map char pin_profd[PATH_MAX]; int pin_profd_len = snprintf(pin_profd, PATH_MAX, "%s/%s", prog_root_sane, "profd"); if (pin_profd_len >= PATH_MAX) { argp_error(state, "data pinning path too long"); } args->pin[1] = pin_profd; // Create pinning path for the names map char pin_profn[PATH_MAX]; int pin_profn_len = snprintf(pin_profn, PATH_MAX, "%s/%s", prog_root_sane, "profn"); if (pin_profn_len >= PATH_MAX) { argp_error(state, "names pinning path too long"); } args->pin[2] = pin_profn; // Create pinning path for the coverage mapping header char pin_covmap[PATH_MAX]; int pin_covmap_len = snprintf(pin_covmap, PATH_MAX, "%s/%s", prog_root_sane, "covmap"); if (pin_covmap_len >= PATH_MAX) { argp_error(state, "coverage mapping header path too long"); } args->pin[3] = pin_covmap; // Check whether the map pinning paths already exist: // - unpin them in case they do exist and the current subcommand is `run` // - error out in case the do not exist and the current subcommand is `gen` handle_map_pins(args, state, is_run); break; default: log_debu(args, "parsing <root> UNKNOWN = '%s'\n", arg ? arg : "(null)"); return ARGP_ERR_UNKNOWN; } return 0; } void root(int argc, char **argv) { struct root_args this = { .verbosity = 0, }; argp_parse(&root_argp, argc, argv, ARGP_IN_ORDER, NULL, &this); if (this.command) { this.command(&this); } else { log_fata(NULL, "%s\n", "not implemented yet"); // run(&this); // gen(&this); } } // -------------------------------------------------------------------------------------------------------------------- // CLI / bpfcov run // -------------------------------------------------------------------------------------------------------------------- struct run_args { struct root_args *parent; }; static struct argp_option run_opts[] = { {"GLOBALS:", 0, 0, OPTION_DOC, 0, 0}, {0} // . }; static char run_docs[] = "\n" "Execute your bpfcov instrumented eBPF applications.\n" "\n"; static struct argp run_argp = { .options = run_opts, .parser = run_parse, .args_doc = "<program>", .doc = run_docs, }; static error_t run_parse(int key, char *arg, struct argp_state *state) { struct run_args *args = state->input; assert(args); assert(args->parent); char str[2]; log_debu(args->parent, "parsing <run> %s = '%s'\n", argp_key(key, str), arg ? arg : "(null)"); switch (key) { case ARGP_KEY_ARG: args->parent->program[state->arg_num] = arg; break; case ARGP_KEY_END: if (!args->parent->program[0]) { argp_error(state, "missing program argument"); } if (access(args->parent->program[0], F_OK) != 0) { argp_error(state, "program '%s' does not actually exist", args->parent->program[0]); } break; default: log_debu(args->parent, "parsing <run> UNKNOWN = '%s'\n", arg ? arg : "(null)"); return ARGP_ERR_UNKNOWN; } return 0; } void run_cmd(struct argp_state *state) { struct run_args args = {}; int argc = state->argc - state->next + 1; char **argv = &state->argv[state->next - 1]; char *argv0 = argv[0]; args.parent = state->input; log_debu(args.parent, "begin <run> (argc = %d, argv[0] = %s)\n", argc, argv[0]); argv[0] = malloc(strlen(state->name) + strlen(" run") + 1); if (!argv[0]) { argp_failure(state, 1, ENOMEM, 0); } sprintf(argv[0], "%s run", state->name); argp_parse(&run_argp, argc, argv, ARGP_IN_ORDER, &argc, &args); free(argv[0]); argv[0] = argv0; state->next += argc - 1; log_debu(args.parent, "end <run> (next = %d, argv[next] = %s)\n", state->next, state->argv[state->next]); } // -------------------------------------------------------------------------------------------------------------------- // CLI / bpfcov gen // -------------------------------------------------------------------------------------------------------------------- struct gen_args { struct root_args *parent; }; const char GEN_OUTPUT_OPT_KEY = 'o'; const char GEN_OUTPUT_OPT_LONG[] = "output"; const char GEN_OUTPUT_OPT_ARG[] = "path"; const char GEN_UNPIN_OPT_KEY = 0x81; const char GEN_UNPIN_OPT_LONG[] = "unpin"; static struct argp_option gen_opts[] = { {"OPTIONS:", 0, 0, OPTION_DOC, 0, 0}, {GEN_OUTPUT_OPT_LONG, GEN_OUTPUT_OPT_KEY, GEN_OUTPUT_OPT_ARG, 0, "Set the output path\n(defaults to <program>.profraw)", 1}, {GEN_UNPIN_OPT_LONG, GEN_UNPIN_OPT_KEY, 0, 0, "Unpin the maps", 1}, {"\n", 0, 0, OPTION_DOC, 0, 0}, {"GLOBALS:", 0, 0, OPTION_DOC, 0, 0}, {0} // . }; static char gen_docs[] = "\n" "Generate the profraw file for the bpfcov instrumented eBPF applications.\n" "\n"; static struct argp gen_argp = { .options = gen_opts, .parser = gen_parse, .args_doc = "<program>", .doc = gen_docs, }; static error_t gen_parse(int key, char *arg, struct argp_state *state) { struct gen_args *args = state->input; assert(args); assert(args->parent); char str[2]; log_debu(args->parent, "parsing <gen> %s = '%s'\n", argp_key(key, str), arg ? arg : "(null)"); switch (key) { case GEN_OUTPUT_OPT_KEY: if (strlen(arg) > 0) { args->parent->output = arg; break; } argp_error(state, "option '--%s' requires a %s", GEN_OUTPUT_OPT_LONG, GEN_OUTPUT_OPT_ARG); break; case GEN_UNPIN_OPT_KEY: args->parent->unpin = true; break; case ARGP_KEY_ARG: // NOTE > Collecting also other arguments/options even though they are not used to generate the pinning path args->parent->program[state->arg_num] = arg; break; case ARGP_KEY_END: if (!args->parent->program[0]) { argp_error(state, "missing program argument"); } if (access(args->parent->program[0], F_OK) != 0) { argp_error(state, "program '%s' does not actually exist", args->parent->program[0]); } if (!args->parent->output) { char output_path[PATH_MAX]; int output_path_len = snprintf(output_path, PATH_MAX, "%s.%s", args->parent->program[0], "profraw"); if (output_path_len >= PATH_MAX) { argp_error(state, "default output path too long"); } args->parent->output = output_path; } break; default: log_debu(args->parent, "parsing <gen> UNKNOWN = '%s'\n", arg ? arg : "(null)"); return ARGP_ERR_UNKNOWN; } return 0; } void gen_cmd(struct argp_state *state) { struct gen_args args = {}; int argc = state->argc - state->next + 1; char **argv = &state->argv[state->next - 1]; char *argv0 = argv[0]; args.parent = state->input; log_debu(args.parent, "begin <gen> (argc = %d, argv[0] = %s)\n", argc, argv[0]); argv[0] = malloc(strlen(state->name) + strlen(" gen") + 1); if (!argv[0]) { argp_failure(state, 1, ENOMEM, 0); } sprintf(argv[0], "%s gen", state->name); argp_parse(&gen_argp, argc, argv, ARGP_IN_ORDER, &argc, &args); free(argv[0]); argv[0] = argv0; state->next += argc - 1; log_debu(args.parent, "end <gen> (next = %d, argv[next] = %s)\n", state->next, state->argv[state->next]); } // -------------------------------------------------------------------------------------------------------------------- // CLI / bpfcov out // -------------------------------------------------------------------------------------------------------------------- struct out_args { struct root_args *parent; }; const char OUT_OUTPUT_OPT_KEY = 'o'; const char OUT_OUTPUT_OPT_LONG[] = "output"; const char OUT_OUTPUT_OPT_ARG[] = "path"; const char OUT_FORMAT_OPT_KEY = 'f'; const char OUT_FORMAT_OPT_LONG[] = "format"; const char OUT_FORMAT_OPT_ARG[] = "html|json|lcov"; static struct argp_option out_opts[] = { {"OPTIONS:", 0, 0, OPTION_DOC, 0, 0}, {OUT_OUTPUT_OPT_LONG, OUT_OUTPUT_OPT_KEY, OUT_OUTPUT_OPT_ARG, 0, " Set the output path\n (defaults to out[_html/|.json|.lcov])", 1}, {OUT_FORMAT_OPT_LONG, OUT_FORMAT_OPT_KEY, OUT_FORMAT_OPT_ARG, 0, "Set the output format\n (defaults to html)", 1}, {"\n", 0, 0, OPTION_DOC, 0, 0}, {"GLOBALS:", 0, 0, OPTION_DOC, 0, 0}, {0} // . }; static char out_docs[] = "\n" "Generate the coverage reports from 1 or more profraw files.\n" "\n"; static struct argp out_argp = { .options = out_opts, .parser = out_parse, .args_doc = "<profraw>+", .doc = out_docs, }; static error_t out_parse(int key, char *arg, struct argp_state *state) { struct out_args *args = state->input; assert(args); assert(args->parent); char str[2]; log_debu(args->parent, "parsing <out> %s = '%s'\n", argp_key(key, str), arg ? arg : "(null)"); switch (key) { case ARGP_KEY_INIT: args->parent->profraw = calloc(PATH_MAX, sizeof(char *)); args->parent->num_profraw = 0; break; case OUT_OUTPUT_OPT_KEY: if (strlen(arg) > 0) { args->parent->report_path = arg; break; } argp_error(state, "option '--%s' requires a %s", OUT_OUTPUT_OPT_LONG, OUT_OUTPUT_OPT_ARG); break; case OUT_FORMAT_OPT_KEY: if (strlen(arg) > 0) { /**/ if (strncmp(arg, "html", 4) == 0) { args->parent->out_format = FORMAT_html; } else if (strncmp(arg, "json", 4) == 0) { args->parent->out_format = FORMAT_json; } else if (strncmp(arg, "lcov", 4) == 0) { args->parent->out_format = FORMAT_lcov; } /**/ else { goto out_format_error; } break; } out_format_error: argp_error(state, "option '--%s' requires a value (%s)", OUT_FORMAT_OPT_LONG, OUT_FORMAT_OPT_ARG); break; case ARGP_KEY_ARG: assert(arg); args->parent->profraw[state->arg_num] = arg; break; case ARGP_KEY_END: if (args->parent->profraw[0] == NULL) { argp_error(state, "at least one profraw input file is required"); } char** ptr = args->parent->profraw; for (char* profraw = *ptr; profraw; profraw = *++ptr) { if (access(profraw, F_OK) != 0) { argp_error(state, "input profraw file '%s' does not actually exist", profraw); } // TODO(leodido) > check it really is a profraw file? args->parent->num_profraw++; } if (!args->parent->report_path) { char *sep = "."; switch (args->parent->out_format) { case FORMAT_html: sep = "_"; break; default: break; } char report_path[PATH_MAX]; int report_path_len = snprintf(report_path, PATH_MAX, "%s%s%s", "out", sep, format_string[args->parent->out_format]); if (report_path_len >= PATH_MAX) { argp_error(state, "default output path too long"); } args->parent->report_path = report_path; } break; default: log_debu(args->parent, "parsing <out> UNKNOWN = '%s'\n", arg ? arg : "(null)"); return ARGP_ERR_UNKNOWN; } return 0; } void out_cmd(struct argp_state *state) { struct out_args args = {}; int argc = state->argc - state->next + 1; char **argv = &state->argv[state->next - 1]; char *argv0 = argv[0]; args.parent = state->input; log_debu(args.parent, "begin <out> (argc = %d, argv[0] = %s)\n", argc, argv[0]); argv[0] = malloc(strlen(state->name) + strlen(" out") + 1); if (!argv[0]) { argp_failure(state, 1, ENOMEM, 0); } sprintf(argv[0], "%s out", state->name); argp_parse(&out_argp, argc, argv, ARGP_IN_ORDER, &argc, &args); free(argv[0]); argv[0] = argv0; state->next += argc - 1; log_debu(args.parent, "end <out> (next = %d, argv[next] = %s)\n", state->next, state->argv[state->next]); } // -------------------------------------------------------------------------------------------------------------------- // Miscellaneous // -------------------------------------------------------------------------------------------------------------------- const char *argp_key(int key, char *str) { str[0] = key; str[1] = 0; switch (key) { case ARGP_KEY_ARG: return "ARGP_KEY_ARG"; case ARGP_KEY_ARGS: return "ARGP_KEY_ARGS"; case ARGP_KEY_END: return "ARGP_KEY_END"; case ARGP_KEY_NO_ARGS: return "ARGP_KEY_NO_ARGS"; case ARGP_KEY_INIT: return "ARGP_KEY_INIT"; case ARGP_KEY_SUCCESS: return "ARGP_KEY_SUCCESS"; case ARGP_KEY_ERROR: return "ARGP_KEY_ERROR"; case ARGP_KEY_FINI: return "ARGP_KEY_FINI"; } return str; }; void print_log(int level, const char *prefix, struct root_args *args, const char *fmt, ...) { if (args->verbosity < level) { return; } FILE *f = level == 0 ? stderr : stdout; va_list argptr; va_start(argptr, fmt); if (!prefix || (prefix && !*prefix)) { goto without_prefix; } char *category = "unkn"; switch (level) { case 0: category = "erro"; break; case 1: category = "warn"; break; case 2: category = "info"; break; case 3: category = "debu"; break; } fprintf(f, "%s: %s: ", TOOL_NAME, category); without_prefix: vfprintf(f, fmt, argptr); va_end(argptr); } static bool is_bpffs(char *bpffs_path) { struct statfs st_fs; if (statfs(bpffs_path, &st_fs) < 0) return false; return st_fs.f_type == BPF_FS_MAGIC; } static void strip_trailing_char(char *str, char c) { int last = strlen(str) - 1; while (last > 0 && str[last] == c) { str[last--] = '\0'; } } static void replace_with(char *str, const char what, const char with) { while (*str) { if (*str == what) { *str = with; } str++; } } static void strip_extension(char *str) { char *end = str + strlen(str); while (end > str && *end != '.' && *end != '\\' && *end != '/') { --end; } if ((end > str && *end == '.') && (*(end - 1) != '\\' && *(end - 1) != '/')) { *end = '\0'; } } static void handle_map_pins(struct root_args *args, struct argp_state *state, bool unpin) { int p; for (p = 0; p < NUM_PINNED_MAPS; p++) { if (access(args->pin[p], F_OK) == 0) { if (unpin) { log_warn(args, "unpinning existing map '%s'\n", args->pin[p]); if (unlink(args->pin[p]) != 0) { if (state) { argp_error(state, "could not unpin map '%s'", args->pin[p]); } else { log_fata(args, "could not unpin map '%s'\n", args->pin[p]); } } } } else { if (!unpin) { if (state) { argp_error(state, "could not access map '%s'", args->pin[p]); } else { log_fata(args, "could not access map '%s'\n", args->pin[p]); } } } } } static int get_pin_path(struct root_args *args, char *suffix, char **pin_path) { if (!suffix) { return 0; } if (strncmp(suffix, "profc", 5) == 0) { *pin_path = args->pin[0]; return 1; } else if (strncmp(suffix, "profd", 5) == 0) { *pin_path = args->pin[1]; return 1; } else if (strncmp(suffix, "profn", 5) == 0) { *pin_path = args->pin[2]; return 1; } else if (strncmp(suffix, "covmap", 6) == 0) { *pin_path = args->pin[3]; return 1; } return 0; } static int get_map_info(int fd, struct bpf_map_info *info) { int err; unsigned int size = sizeof(*info); memset(info, 0, size); err = bpf_obj_get_info_by_fd(fd, info, &size); if (err) { close(fd); } return err; } static int get_global_data(int fd, struct bpf_map_info *info, void *data) { int err; void *k, *v; k = malloc(info->key_size); v = malloc(info->value_size); if (!k || !v) { err = -1; goto error_out; } if (!info) { err = -1; goto error_out; } if (info->max_entries > 1) { err = -1; goto error_out; } err = bpf_map_get_next_key(fd, NULL, k); if (err) { goto error_out; } err = bpf_map_lookup_elem(fd, k, v); if (err) { goto error_out; } memcpy(data, v, info->value_size); error_out: free(k); free(v); close(fd); return err; } static void wait_or_exit(struct root_args *args, pid_t pid, char *err) { if (!err) { err = "exited with status"; } int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { int exit_status = WEXITSTATUS(status); if (exit_status != 0) { log_fata(args, "%s %d\n", err, exit_status); } } } // -------------------------------------------------------------------------------------------------------------------- // Implementation // -------------------------------------------------------------------------------------------------------------------- int run(struct root_args *args) { log_info(args, "executing program '%s'\n", args->program[0]); pid_t pid = fork(); switch (pid) { case -1: /* Error */ log_fata(args, "%s\n", strerror(errno)); case 0: /* Child */ if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) { log_fata(args, "%s\n", strerror(errno)); } execvp(args->program[0], args->program); log_fata(args, "%s\n", strerror(errno)); } /* Parent */ waitpid(pid, 0, 0); // sync with PTRACE_TRACEME ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_EXITKILL); int is_map = 0; for (;;) { /* Enter next system call */ if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) { log_fata(args, "%s\n", strerror(errno)); } /* Waiting for PID to die */ if (waitpid(pid, 0, 0) == -1) { log_fata(args, "%s\n", strerror(errno)); } /* Gather system call arguments */ struct user_regs_struct regs; if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1) { log_fata(args, "%s\n", strerror(errno)); } /* Mark bpf(BPF_MAP_CREATE, ...) */ const unsigned int sysc = regs.orig_rax; const unsigned int comm = regs.rdi; is_map = (sysc == SYS_bpf && comm == BPF_MAP_CREATE); /* Print a representation of the system call */ log_debu(args, "%d(%d, %ld, %ld, %ld, %ld, %ld)", sysc, comm, (long)regs.rsi, (long)regs.rdx, (long)regs.r10, (long)regs.r8, (long)regs.r9); /* Run system call and stop on exit */ if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) { log_fata(args, "%s\n", strerror(errno)); } /* Waiting for PID to die */ if (waitpid(pid, 0, 0) == -1) { log_fata(args, "%s\n", strerror(errno)); } /* Get system call result */ if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1) { if (DEBUG) { print_log(3, NULL, args, "%s\n", " = ?"); } if (errno == ESRCH) { exit(regs.rdi); // _exit(2) or similar } log_fata(args, "%s\n", strerror(errno)); } /* Print system call result */ long result = regs.rax; if (DEBUG) { print_log(3, NULL, args, " = %ld\n", result); } /* Pin the bpfcov maps */ if (is_map && result) { int pidfd = syscall(SYS_pidfd_open, pid, 0); if (pidfd < 0) { continue; } int curfd = syscall(SYS_pidfd_getfd, pidfd, result, 0); if (curfd < 0) { continue; } close(pidfd); struct bpf_map_info map_info = {}; int err; err = get_map_info(curfd, &map_info); if (!err && strlen(map_info.name) > 0) { log_info(args, "got info about map '%s'\n", map_info.name); char map_name[BPF_OBJ_NAME_LEN]; strcpy(map_name, map_info.name); const char *sep = "."; strtok(map_info.name, sep); char *suffix = strtok(NULL, sep); char *pin_path = ""; if (get_pin_path(args, suffix, &pin_path)) { err = bpf_obj_pin(curfd, pin_path); if (err) { if (errno == EEXIST) { log_warn(args, "pin '%s' already exists for map '%s'\n", pin_path, map_name); continue; } log_fata(args, "%s\n", "could not pin map"); } log_warn(args, "pin map '%s' to '%s'\n", map_name, pin_path); } } } } return 0; } int gen(struct root_args *args) { log_info(args, "generating '%s' for program '%s'\n", args->output, args->program[0]); /* Get maps info */ struct bpf_map_info profc_info = {}; if (get_map_info(bpf_obj_get(args->pin[0]), &profc_info)) { log_fata(args, "could not get info about pinned map '%s'\n", args->pin[0]); } struct bpf_map_info profd_info = {}; if (get_map_info(bpf_obj_get(args->pin[1]), &profd_info)) { log_fata(args, "could not get info about pinned map '%s'\n", args->pin[1]); } struct bpf_map_info profn_info = {}; if (get_map_info(bpf_obj_get(args->pin[2]), &profn_info)) { log_fata(args, "could not get info about pinned map '%s'\n", args->pin[2]); } struct bpf_map_info covmap_info = {}; if (get_map_info(bpf_obj_get(args->pin[3]), &covmap_info)) { log_fata(args, "could not get info about pinned map '%s'\n", args->pin[3]); } /* Time to write binary data to the output file */ FILE *outfp = fopen(args->output, "wb"); if (!outfp) { log_fata(args, "could not open the output file '%s'\n", args->output); } /* Write the header */ log_info(args, "%s\n", "about to write the profraw header..."); // Magic number char magic[8] = {0x81, 0x72, 0x66, 0x6F, 0x72, 0x70, 0x6C, 0xFF}; fwrite(magic, 1, sizeof(magic), outfp); // Version void *covmap_data = malloc(covmap_info.value_size); if (get_global_data(bpf_obj_get(args->pin[3]), &covmap_info, covmap_data)) { fclose(outfp); log_fata(args, "could not get global data from map '%s'\n", args->pin[3]); } long long int version = 0; memcpy(&version, &((char *)covmap_data)[12], 4); // Version is the 3rd int in the coverage mapping header version += 1; // Version is 0 indexed fwrite(&version, 1, sizeof(version), outfp); free(covmap_data); // Data size long long int func_num = profd_info.value_size / 48; // 5 x i64 + 2 x i32 for each function fwrite(&func_num, 1, sizeof(func_num), outfp); // Padding before counters long long int pad_bef = 0; fwrite(&pad_bef, 1, sizeof(pad_bef), outfp); // Counters size long long int counters_num = profc_info.value_size / 8; // 1 x i64 for each counter element fwrite(&counters_num, 1, sizeof(counters_num), outfp); // Padding after counters long long int pad_aft = 0; fwrite(&pad_aft, 1, sizeof(pad_aft), outfp); // Names size long long int names_sz = profn_info.value_size; fwrite(&names_sz, 1, sizeof(names_sz), outfp); // Counters delta (nulled) long long int counters_delta = 0; fwrite(&counters_delta, 1, sizeof(counters_delta), outfp); // Names delta (nulled) long long int names_delta = 0; fwrite(&names_delta, 1, sizeof(names_delta), outfp); // IPVK last long long int ipvk_last = 1; fwrite(&ipvk_last, 1, sizeof(ipvk_last), outfp); /* Write the data part */ log_info(args, "%s\n", "about to write the data in the profraw..."); void *profd_data = malloc(profd_info.value_size); if (get_global_data(bpf_obj_get(args->pin[1]), &profd_info, profd_data)) { fclose(outfp); log_fata(args, "could not get global data from map '%s'\n", args->pin[1]); } fwrite(profd_data, profd_info.value_size, 1, outfp); /* Write the counters part */ log_info(args, "%s\n", "about to write the counters in the profraw.."); void *profc_data = malloc(profc_info.value_size); if (get_global_data(bpf_obj_get(args->pin[0]), &profc_info, profc_data)) { fclose(outfp); log_fata(args, "could not get global data from map '%s'\n", args->pin[0]); } fwrite(profc_data, profc_info.value_size, 1, outfp); /* Write the names part */ log_info(args, "%s\n", "about to write the names in the profraw..."); void *profn_data = malloc(profn_info.value_size); if (get_global_data(bpf_obj_get(args->pin[2]), &profn_info, profn_data)) { fclose(outfp); log_fata(args, "could not get global data from map '%s'\n", args->pin[2]); } fwrite(profn_data, profn_info.value_size, 1, outfp); /* Align to 8 bytes */ unsigned int b = 0; for (unsigned int p = b; p < (7 & (16 - profn_info.value_size % 16)); p++) { fwrite(&b, 1, 1, outfp); } /* Close */ fclose(outfp); /* Unpin the maps */ handle_map_pins(args, NULL, args->unpin); return 0; } int out(struct root_args *args) { log_info(args, "%s\n", "generating coverage visualization..."); char report_path[PATH_MAX]; strcpy(report_path, args->report_path); // Generating a *.profdata for each input *.profraw char profdata[args->num_profraw][PATH_MAX]; memset(profdata, 0, args->num_profraw * PATH_MAX * sizeof(char)); char bpfobjs[args->num_profraw][PATH_MAX]; memset(bpfobjs, 0, args->num_profraw * PATH_MAX * sizeof(char)); int c = 0; char** ptr = args->profraw; for (char* profraw = *ptr; profraw; profraw = *++ptr) { // Looking up for *.bpf.obj file sibling to the current input *.profraw file char *profraw_wo_ext = strdup(profraw); strip_extension(profraw_wo_ext); char bpfobj_path[PATH_MAX]; int bpfobj_path_len = snprintf(bpfobj_path, PATH_MAX, "%s.bpf.obj", profraw_wo_ext); if (bpfobj_path_len >= PATH_MAX) { log_fata(args, "%s\n", "bpf.obj output path too long"); } log_info(args, "looking for BPF coverage object at '%s'\n", bpfobj_path); if (access(bpfobj_path, F_OK) != 0) { log_fata(args, "could not find the BPF coverage object at '%s'", bpfobj_path); } free(profraw_wo_ext); // Storing the *.bpf.obj file for later strncpy(bpfobjs[c], bpfobj_path, PATH_MAX); // Creating the *.profdata path (relative to the execution directory) char *profraw_name = strdup(basename(profraw)); strtok(profraw_name, "."); char profdata_path[PATH_MAX]; int profdata_path_len = snprintf(profdata_path, PATH_MAX, "%s.profdata", profraw_name); if (profdata_path_len >= PATH_MAX) { log_fata(args, "%s\n", "profdata output path too long"); } free(profraw_name); // Storing the output *.profdata file for later strncpy(profdata[c], profdata_path, PATH_MAX); log_info(args, "generating '%s'\n", profdata[c]); // Generating the single *.profdata file pid_t data_pid; switch ((data_pid = fork())) { case -1: log_fata(args, "%s\n", "could not fork"); break; case 0: log_debu(args, "llvm-profdata merge -sparse %s -o %s\n", profraw, profdata[c]); int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); dup2(devnull, STDERR_FILENO); execlp("llvm-profdata", "llvm-profdata", "merge", "-sparse", profraw, "-o", profdata[c], NULL); close(devnull); log_fata(args, "%s\n", "could not exec llvm-profdata"); break; } wait_or_exit(args, data_pid, "llvm-profdata: exited with status"); c++; } // Merge all the *.profdata into one char target_profdata[PATH_MAX]; int num_profdata = sizeof(profdata) / PATH_MAX; if (num_profdata > 1) { strncpy(target_profdata, "all.profdata", PATH_MAX); log_info(args, "merging into '%s'\n", target_profdata); pid_t merge_pid; switch ((merge_pid = fork())) { case -1: log_fata(args, "%s\n", "could not fork"); break; case 0: char *arguments[num_profdata + 6]; arguments[0] = "llvm-profdata"; arguments[1] = "merge"; arguments[2] = "-sparse"; arguments[3] = "-o"; arguments[4] = target_profdata; for (int i = 0; i < num_profdata; i++) { arguments[i + 5] = profdata[i]; } arguments[num_profdata + 5] = NULL; log_debu(args, "%s ", arguments[0]); for (int a = 1; a < num_profdata + 5; a++) { if (DEBUG) { print_log(3, NULL, args, "%s ", arguments[a]); } } if (DEBUG) { print_log(3, NULL, args, "%s", "\n"); } int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); dup2(devnull, STDERR_FILENO); execvp("llvm-profdata", arguments); close(devnull); log_fata(args, "%s\n", "could not exec llvm-profdata"); break; } wait_or_exit(args, merge_pid, "llvm-profdata: exited with status"); } else { strncpy(target_profdata, profdata[0], PATH_MAX); } int num_bpfobj_params = num_profdata * 2; int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); if (devnull == -1) { log_fata(args, "could not open %s\n", "/dev/null"); } log_info(args, "about to generate the %s coverage report in '%s'\n", format_string[args->out_format], report_path); switch (args->out_format) { case FORMAT_html: pid_t html_pid; switch ((html_pid = fork())) { case -1: log_fata(args, "%s\n", "could not fork"); break; case 0: char *arguments[num_bpfobj_params + 11]; arguments[0] = "llvm-cov"; arguments[1] = "show"; arguments[2] = "--format=html"; arguments[3] = "--show-branches=count"; arguments[4] = "--show-line-counts-or-regions"; arguments[5] = "--show-region-summary"; arguments[6] = "--output-dir"; arguments[7] = report_path; arguments[8] = "-instr-profile"; arguments[9] = target_profdata; for (int i = 0; i < num_profdata; i++) { int off = i * 2; arguments[off + 10] = "-object"; arguments[off + 11] = bpfobjs[i]; } arguments[num_bpfobj_params + 10] = NULL; log_debu(args, "%s ", arguments[0]); for (int a = 1; a < num_bpfobj_params + 10; a++) { if (DEBUG) { print_log(3, NULL, args, "%s ", arguments[a]); } } if (DEBUG) { print_log(3, NULL, args, "%s", "\n"); } dup2(devnull, STDERR_FILENO); execvp("llvm-cov", arguments); break; } close(devnull); wait_or_exit(args, html_pid, "llvm-cov exited with status"); break; case FORMAT_lcov: /* Fallthrough */ case FORMAT_json: int outfile = open(report_path, O_RDWR | O_CREAT, 0600); if (outfile == -1) { log_fata(args, "could not open %s\n", report_path); } pid_t export_pid; switch ((export_pid = fork())) { case -1: log_fata(args, "%s\n", "could not fork"); break; case 0: char *arguments[num_bpfobj_params + 8]; arguments[0] = "llvm-cov"; arguments[1] = "export"; arguments[2] = "--format=text"; arguments[3] = "--show-branch-summary"; arguments[4] = "--show-region-summary"; arguments[5] = "-instr-profile"; arguments[6] = target_profdata; for (int i = 0; i < num_profdata; i++) { int off = i > 0 ? (i + 1) : i; arguments[off + 7] = "-object"; arguments[off + 8] = bpfobjs[i]; } arguments[num_bpfobj_params + 7] = NULL; log_debu(args, "%s ", arguments[0]); for (int a = 1; a < num_bpfobj_params + 7; a++) { if (DEBUG) { print_log(3, NULL, args, "%s ", arguments[a]); } } if (DEBUG) { print_log(3, NULL, args, "%s", "\n"); } dup2(devnull, STDERR_FILENO); dup2(outfile, STDOUT_FILENO); execvp("llvm-cov", arguments); break; } close(devnull); close(outfile); wait_or_exit(args, export_pid, "llvm-cov exited with status"); break; } return 0; }