diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 77f96f7ec488..4e98cf385d6a 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,11 +1,16 @@ -file(GLOB C_FILES *.c) +file(GLOB C_FILES *.c *.h) file(GLOB PY_FILES *.py) file(GLOB SH_FILES *.sh) file(GLOB TXT_FILES *.txt) list(REMOVE_ITEM TXT_FILES ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt) foreach(FIL ${PY_FILES}) get_filename_component(FIL_WE ${FIL} NAME_WE) - install(PROGRAMS ${FIL} DESTINATION share/bcc/tools RENAME ${FIL_WE}) + # python helpers for import need suffix .py + if (FIL_WE MATCHES "_helpers") + install(FILES ${FIL} DESTINATION share/bcc/tools) + else() + install(PROGRAMS ${FIL} DESTINATION share/bcc/tools RENAME ${FIL_WE}) + endif() endforeach() foreach(FIL ${SH_FILES}) if(${FIL} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}/reset-trace.sh) diff --git a/tools/filegone.py b/tools/filegone.py index 9b8c01684a19..f389b637f343 100755 --- a/tools/filegone.py +++ b/tools/filegone.py @@ -11,6 +11,7 @@ # 08-Nov-2022 Curu. modified from filelife # 19-Nov-2022 Rong Tao Check btf struct field instead of KERNEL_VERSION macro. # 05-Nov-2023 Rong Tao Support rename/unlink failed situation. +# 01-Jul-2025 Rong Tao Support file path from __future__ import print_function from bcc import BPF @@ -20,6 +21,7 @@ # arguments examples = """examples: ./filegone # trace all file gone events + ./filegone -P # show file path ./filegone -p 181 # only trace PID 181 """ parser = argparse.ArgumentParser( @@ -28,6 +30,8 @@ epilog=examples) parser.add_argument("-p", "--pid", help="trace this PID only") +parser.add_argument("-P", "--path", action="store_true", + help="show file path") parser.add_argument("--ebpf", action="store_true", help=argparse.SUPPRESS) args = parser.parse_args() @@ -37,18 +41,46 @@ bpf_text = """ #include #include +#include #include +#ifdef FULLPATH +INCLUDE_FULL_PATH_H +INCLUDE_PATH_HELPERS_BPF_H +#endif struct data_t { u32 pid; u8 action; char comm[TASK_COMM_LEN]; - char fname[DNAME_INLINE_LEN]; - char fname2[DNAME_INLINE_LEN]; + u32 path_depth, path_depth2; +#ifdef FULLPATH + FULL_PATH_FIELD(fname); + FULL_PATH_FIELD(fname2); +#else + char fname[NAME_MAX]; + char fname2[NAME_MAX]; +#endif }; -BPF_PERF_OUTPUT(events); -BPF_HASH(currdata, u32, struct data_t); +struct entry_t { + u32 pid; + u8 action; + struct { + char name[DNAME_INLINE_LEN]; + struct dentry *dentry; + } old, new; +}; + +BPF_RINGBUF_OUTPUT(events, 64); +BPF_HASH(currentry, u32, struct entry_t); + +static inline void get_dentry_name(char **name, struct dentry *dentry) +{ + struct qstr d_name = dentry->d_name; + if (d_name.len == 0) + return; + bpf_probe_read_kernel(&data.fname, sizeof(data.fname), d_name.name); +} // trace file deletion and output details TRACE_VFS_UNLINK_FUNC @@ -59,17 +91,14 @@ FILTER - struct data_t data = {}; - struct qstr d_name = dentry->d_name; - if (d_name.len == 0) - return 0; + struct entry_t entry = {}; - bpf_get_current_comm(&data.comm, sizeof(data.comm)); - data.pid = pid; - data.action = 'D'; - bpf_probe_read_kernel(&data.fname, sizeof(data.fname), d_name.name); + entry.pid = pid; + entry.action = 'D'; + entry.old.dentry = dentry; + get_dentry_name((char **)&entry.old.name, dentry); - currdata.update(&tid, &data); + currentry.update(&tid, &entry); return 0; } @@ -77,24 +106,26 @@ // trace file rename TRACE_VFS_RENAME_FUNC + struct entry_t entry = {}; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; FILTER - struct data_t data = {}; - struct qstr s_name = old_dentry->d_name; - struct qstr d_name = new_dentry->d_name; - if (s_name.len == 0 || d_name.len == 0) - return 0; + entry.pid = pid; + entry.action = 'R'; + + /** + * Couldn't get new and old dentry name in trace_return(), because you'll + * get new-name for old. + */ + entry.old.dentry = old_dentry; + get_dentry_name((char **)&entry.old.name, old_dentry); + entry.new.dentry = new_dentry; + get_dentry_name((char **)&entry.new.name, new_dentry); - bpf_get_current_comm(&data.comm, sizeof(data.comm)); - data.pid = pid; - data.action = 'R'; - bpf_probe_read_kernel(&data.fname, sizeof(data.fname), s_name.name); - bpf_probe_read_kernel(&data.fname2, sizeof(data.fname), d_name.name); - currdata.update(&tid, &data); + currentry.update(&tid, &entry); return 0; } @@ -102,20 +133,58 @@ int trace_return(struct pt_regs *ctx) { struct data_t *data; + struct entry_t *entry; u32 tid = (u32)bpf_get_current_pid_tgid(); int ret = PT_REGS_RC(ctx); - data = currdata.lookup(&tid); - if (data == 0) + entry = currentry.lookup(&tid); + if (entry == 0) return 0; - currdata.delete(&tid); + currentry.delete(&tid); /* Skip failed */ if (ret) return 0; - events.perf_submit(ctx, data, sizeof(*data)); + data = events.ringbuf_reserve(sizeof(struct data_t)); + if (!data) + return 0; + + data->pid = entry->pid; + data->action = entry->action; + bpf_get_current_comm(&data->comm, sizeof(data->comm)); + + bpf_probe_read(&data->fname, sizeof(data->fname), entry->old.name); + + data->path_depth = data->path_depth2 = 0; + if (entry->action == 'R') + bpf_probe_read(&data->fname2, sizeof(data->fname2), entry->new.name); + +#ifdef FULLPATH + struct task_struct *task; + struct fs_struct *fs; + struct vfsmount *cwd_vfsmnt; + + task = (struct task_struct *)bpf_get_current_task_btf(); + bpf_probe_read_kernel(&fs, sizeof(fs), &task->fs); + bpf_probe_read_kernel(&cwd_vfsmnt, sizeof(cwd_vfsmnt), &fs->pwd.mnt); + if (data->fname[0] != '/') { + data->path_depth = 1; + bpf_dentry_full_path(data->fname + NAME_MAX, NAME_MAX, MAX_ENTRIES - 1, + entry->old.dentry->d_parent, cwd_vfsmnt, + &data->path_depth); + } + + if (entry->action == 'R' && data->fname2[0] != '/') { + data->path_depth2 = 1; + bpf_dentry_full_path(data->fname2 + NAME_MAX, NAME_MAX, MAX_ENTRIES - 1, + entry->new.dentry->d_parent, cwd_vfsmnt, + &data->path_depth2); + } +#endif + + events.ringbuf_submit(data, sizeof(*data)); return 0; } """ @@ -155,11 +224,6 @@ def action2str(action): else: bpf_text = bpf_text.replace('FILTER', '') -if debug or args.ebpf: - print(bpf_text) - if args.ebpf: - exit() - # check 'struct renamedata' exist or not if BPF.kernel_struct_has_field(b'renamedata', b'new_mnt_idmap') == 1: bpf_text = bpf_text.replace('TRACE_VFS_RENAME_FUNC', bpf_vfs_rename_text_new) @@ -171,6 +235,23 @@ def action2str(action): bpf_text = bpf_text.replace('TRACE_VFS_RENAME_FUNC', bpf_vfs_rename_text_old) bpf_text = bpf_text.replace('TRACE_VFS_UNLINK_FUNC', bpf_vfs_unlink_text_1) +if args.path: + bpf_text = "#define FULLPATH\n" + bpf_text + + with open(BPF._find_file("full_path.h".encode("utf-8"))) as fileobj: + progtxt = fileobj.read() + bpf_text = bpf_text.replace('INCLUDE_FULL_PATH_H', progtxt) + + with open(BPF._find_file("path_helpers.bpf.c".encode("utf-8"))) as fileobj: + progtxt = fileobj.read() + bpf_text = bpf_text.replace('INCLUDE_PATH_HELPERS_BPF_H', progtxt) + +# NOTE: After bpf_text modification +if debug or args.ebpf: + print(bpf_text) + if args.ebpf: + exit() + # initialize BPF b = BPF(text=bpf_text) b.attach_kprobe(event="vfs_unlink", fn_name="trace_unlink") @@ -187,15 +268,26 @@ def action2str(action): def print_event(cpu, data, size): event = b["events"].event(data) action_str = action2str(event.action) - file_str = event.fname.decode('utf-8', 'replace') - if action_str == "RENAME": - file_str = "%s > %s" % (file_str, event.fname2.decode('utf-8', 'replace')) + if args.path: + import os + import sys + sys.path.append(os.path.dirname(sys.argv[0])) + from path_helpers import get_full_path + file_str = get_full_path(event.fname, event.path_depth) + if action_str == "RENAME": + file2_str = get_full_path(event.fname2, event.path_depth2) + file_str = "%s > %s" % (file_str, file2_str) + else: + file_str = event.fname.decode('utf-8', 'replace') + if action_str == "RENAME": + file2_str = event.fname2.decode('utf-8', 'replace') + file_str = "%s > %s" % (file_str, file2_str) print("%-8s %-7d %-16s %6s %s" % (strftime("%H:%M:%S"), event.pid, event.comm.decode('utf-8', 'replace'), action_str, file_str)) -b["events"].open_perf_buffer(print_event) +b["events"].open_ring_buffer(print_event) while 1: try: - b.perf_buffer_poll() + b.ring_buffer_poll() except KeyboardInterrupt: exit() diff --git a/tools/filegone_example.txt b/tools/filegone_example.txt index ddf8ac907e9a..23b4191a0a6c 100644 --- a/tools/filegone_example.txt +++ b/tools/filegone_example.txt @@ -13,14 +13,17 @@ For example: USAGE message: -usage: filegone.py [-h] [-p PID] +# ./filegone -h +usage: filegone.py [-h] [-p PID] [-P] Trace why file gone (deleted or renamed) -optional arguments: - -h, --help show this help message and exit - -p PID, --pid PID trace this PID only +options: + -h, --help show this help message and exit + -p, --pid PID trace this PID only + -P, --path show file path examples: ./filegone # trace all file gone events + ./filegone -P # show file path ./filegone -p 181 # only trace PID 181 diff --git a/tools/filelife.py b/tools/filelife.py index 1f00dbdcd7e9..2c99c4248b14 100755 --- a/tools/filelife.py +++ b/tools/filelife.py @@ -18,15 +18,18 @@ # 17-Feb-2016 Allan McAleavy updated for BPF_PERF_OUTPUT # 13-Nov-2022 Rong Tao Check btf struct field for CO-RE and add vfs_open() # 05-Nov-2023 Rong Tao Support unlink failed +# 01-Jul-2025 Rong Tao Support file path from __future__ import print_function from bcc import BPF +from bcc.utils import printb import argparse from time import strftime # arguments examples = """examples: ./filelife # trace lifecycle of file(create->remove) + ./filelife -P # show path of file ./filelife -p 181 # only trace PID 181 """ parser = argparse.ArgumentParser( @@ -35,6 +38,8 @@ epilog=examples) parser.add_argument("-p", "--pid", help="trace this PID only") +parser.add_argument("-P", "--path", action="store_true", + help="show file path") parser.add_argument("--ebpf", action="store_true", help=argparse.SUPPRESS) args = parser.parse_args() @@ -45,27 +50,58 @@ #include #include #include +#include +#ifdef FULLPATH +INCLUDE_FULL_PATH_H +INCLUDE_PATH_HELPERS_BPF_H +#endif struct data_t { u32 pid; u64 delta; char comm[TASK_COMM_LEN]; + u32 path_depth; +#ifdef FULLPATH + FULL_PATH_FIELD(fname); +#else char fname[DNAME_INLINE_LEN]; - /* private */ - void *dentry; +#endif }; -BPF_HASH(birth, struct dentry *); -BPF_HASH(unlink_data, u32, struct data_t); -BPF_PERF_OUTPUT(events); +struct create_arg { + u64 ts; + struct vfsmount *cwd_vfsmnt; +}; + +struct unlink_event { + u32 tid; + u64 delta; + struct dentry *dentry; + struct vfsmount *cwd_vfsmnt; +}; + +BPF_HASH(birth, struct dentry *, struct create_arg); +BPF_HASH(unlink_data, u32, struct unlink_event); +BPF_RINGBUF_OUTPUT(events, 64); static int probe_dentry(struct pt_regs *ctx, struct dentry *dentry) { + struct task_struct *task; + struct fs_struct *fs; + struct create_arg arg = {}; u32 pid = bpf_get_current_pid_tgid() >> 32; FILTER u64 ts = bpf_ktime_get_ns(); - birth.update(&dentry, &ts); + + arg.ts = ts; + task = (struct task_struct *)bpf_get_current_task_btf(); + + arg.ts = ts; + bpf_probe_read_kernel(&fs, sizeof(fs), &task->fs); + bpf_probe_read_kernel(&arg.cwd_vfsmnt, sizeof(arg.cwd_vfsmnt), &fs->pwd.mnt); + + birth.update(&dentry, &arg); return 0; } @@ -98,46 +134,41 @@ // trace file deletion and output details TRACE_UNLINK_FUNC { - struct data_t data = {}; + struct create_arg *arg; + struct unlink_event event = {}; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; FILTER - u64 *tsp, delta; - tsp = birth.lookup(&dentry); - if (tsp == 0) { + u64 delta; + arg = birth.lookup(&dentry); + if (arg == 0) { return 0; // missed create } - delta = (bpf_ktime_get_ns() - *tsp) / 1000000; - - struct qstr d_name = dentry->d_name; - if (d_name.len == 0) - return 0; - - if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) { - data.pid = pid; - data.delta = delta; - bpf_probe_read_kernel(&data.fname, sizeof(data.fname), d_name.name); - } + delta = (bpf_ktime_get_ns() - arg->ts) / 1000000; /* record dentry, only delete from birth if unlink successful */ - data.dentry = dentry; + event.delta = delta; + event.tid = tid; + event.dentry = dentry; + event.cwd_vfsmnt = arg->cwd_vfsmnt; - unlink_data.update(&tid, &data); + unlink_data.update(&tid, &event); return 0; } int trace_unlink_ret(struct pt_regs *ctx) { int ret = PT_REGS_RC(ctx); + struct unlink_event *unlink_event; struct data_t *data; u32 tid = (u32)bpf_get_current_pid_tgid(); - data = unlink_data.lookup(&tid); - if (!data) + unlink_event = unlink_data.lookup(&tid); + if (!unlink_event) return 0; /* delete it any way */ @@ -147,8 +178,30 @@ if (ret) return 0; - birth.delete((struct dentry **)&data->dentry); - events.perf_submit(ctx, data, sizeof(*data)); + data = events.ringbuf_reserve(sizeof(struct data_t)); + if (!data) + return 0; + + data->pid = unlink_event->tid; + data->delta = unlink_event->delta; + bpf_get_current_comm(&data->comm, sizeof(data->comm)); + + data->path_depth = 0; + +#ifdef FULLPATH + if (data->fname[0] != '/') { + bpf_dentry_full_path(data->fname, NAME_MAX, MAX_ENTRIES, + unlink_event->dentry, unlink_event->cwd_vfsmnt, + &data->path_depth); + } +#else + struct qstr d_name = unlink_event->dentry->d_name; + bpf_probe_read_kernel_str(&data->fname, sizeof(data->fname), d_name.name); +#endif + + birth.delete((struct dentry **)&unlink_event->dentry); + + events.ringbuf_submit(data, sizeof(*data)); return 0; } @@ -183,10 +236,17 @@ 'if (pid != %s) { return 0; }' % args.pid) else: bpf_text = bpf_text.replace('FILTER', '') -if debug or args.ebpf: - print(bpf_text) - if args.ebpf: - exit() + +if args.path: + bpf_text = "#define FULLPATH\n" + bpf_text + + with open(BPF._find_file("full_path.h".encode("utf-8"))) as fileobj: + progtxt = fileobj.read() + bpf_text = bpf_text.replace('INCLUDE_FULL_PATH_H', progtxt) + + with open(BPF._find_file("path_helpers.bpf.c".encode("utf-8"))) as fileobj: + progtxt = fileobj.read() + bpf_text = bpf_text.replace('INCLUDE_PATH_HELPERS_BPF_H', progtxt) if BPF.kernel_struct_has_field(b'renamedata', b'new_mnt_idmap') == 1: bpf_text = bpf_text.replace('TRACE_CREATE_FUNC', trace_create_text_3) @@ -198,6 +258,12 @@ bpf_text = bpf_text.replace('TRACE_CREATE_FUNC', trace_create_text_1) bpf_text = bpf_text.replace('TRACE_UNLINK_FUNC', trace_unlink_text_1) +# NOTE: After bpf_text modification +if debug or args.ebpf: + print(bpf_text) + if args.ebpf: + exit() + # initialize BPF b = BPF(text=bpf_text) b.attach_kprobe(event="vfs_create", fn_name="trace_create") @@ -216,13 +282,21 @@ # process event def print_event(cpu, data, size): event = b["events"].event(data) - print("%-8s %-7d %-16s %-7.2f %s" % (strftime("%H:%M:%S"), event.pid, - event.comm.decode('utf-8', 'replace'), float(event.delta) / 1000, - event.fname.decode('utf-8', 'replace'))) - -b["events"].open_perf_buffer(print_event) + printb(b"%-8s %-7d %-16s %-7.2f " % (strftime("%H:%M:%S").encode('utf-8', 'replace'), + event.pid, event.comm, float(event.delta) / 1000), nl="") + if args.path: + import os + import sys + sys.path.append(os.path.dirname(sys.argv[0])) + from path_helpers import get_full_path + result = get_full_path(event.fname, event.path_depth) + printb(b"%s" % result.encode("utf-8")) + else: + printb(b"%s" % event.fname) + +b["events"].open_ring_buffer(print_event) while 1: try: - b.perf_buffer_poll() + b.ring_buffer_poll() except KeyboardInterrupt: exit() diff --git a/tools/filelife_example.txt b/tools/filelife_example.txt index 846e3323027f..86836b050456 100644 --- a/tools/filelife_example.txt +++ b/tools/filelife_example.txt @@ -39,14 +39,16 @@ optimizations. USAGE message: # ./filelife -h -usage: filelife [-h] [-p PID] +usage: filelife.py [-h] [-p PID] [-P] -Trace stat() syscalls +Trace lifecycle of file -optional arguments: - -h, --help show this help message and exit - -p PID, --pid PID trace this PID only +options: + -h, --help show this help message and exit + -p, --pid PID trace this PID only + -P, --path show file path examples: - ./filelife # trace all stat() syscalls + ./filelife # trace lifecycle of file(create->remove) + ./filelife -P # show path of file ./filelife -p 181 # only trace PID 181 diff --git a/tools/full_path.h b/tools/full_path.h new file mode 100644 index 000000000000..c7bb221e18f5 --- /dev/null +++ b/tools/full_path.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +/* Copyright (c) 2025 Rong Tao */ +/** + * This code only for bcc/tools/, not for bcc/libbpf-tools/ + */ +#pragma once +#ifndef __FULL_PATH_H +#define __FULL_PATH_H + +#define NAME_MAX 255 +#define MAX_ENTRIES 32 + +/** + * Example: "/CCCCC/BB/AAAA" + * name[]: "AAAA000000000000BB0000000000CCCCC00000000000" + * |<- NAME_MAX ->| + * + * name[] must be u8, because char [] will be truncated by ctypes.cast(), + * such as above example, will be truncated to "AAAA0". + */ +#define FULL_PATH_FIELD(name) u8 name[NAME_MAX * MAX_ENTRIES]; +#endif diff --git a/tools/opensnoop.py b/tools/opensnoop.py index 57c5d5260f65..d5daf3782be8 100755 --- a/tools/opensnoop.py +++ b/tools/opensnoop.py @@ -116,24 +116,10 @@ #include #include #ifdef FULLPATH -#include -#include -#include -#include - -/* see https://github.com/torvalds/linux/blob/master/fs/mount.h */ -struct mount { - struct hlist_node mnt_hash; - struct mount *mnt_parent; - struct dentry *mnt_mountpoint; - struct vfsmount mnt; - /* ... */ -}; +INCLUDE_FULL_PATH_H +INCLUDE_PATH_HELPERS_BPF_H #endif -#define NAME_MAX 255 -#define MAX_ENTRIES 32 - struct val_t { u64 id; char comm[TASK_COMM_LEN]; @@ -150,15 +136,7 @@ char comm[TASK_COMM_LEN]; u32 path_depth; #ifdef FULLPATH - /** - * Example: "/CCCCC/BB/AAAA" - * name[]: "AAAA000000000000BB0000000000CCCCC00000000000" - * |<- NAME_MAX ->| - * - * name[] must be u8, because char [] will be truncated by ctypes.cast(), - * such as above example, will be truncated to "AAAA0". - */ - u8 name[NAME_MAX * MAX_ENTRIES]; + FULL_PATH_FIELD(name); #else /* If not fullpath, avoid transfer big data */ char name[NAME_MAX]; @@ -201,7 +179,9 @@ data->mode = valp->mode; // EXTENDED_STRUCT_MEMBER data->ret = PT_REGS_RC(ctx); - SUBMIT_DATA + GET_FULL_PATH + + events.ringbuf_submit(data, sizeof(*data)); cleanup: infotmp.delete(&id); @@ -362,7 +342,9 @@ data->mode = mode; // EXTENDED_STRUCT_MEMBER data->ret = ret; - SUBMIT_DATA + GET_FULL_PATH + + events.ringbuf_submit(data, sizeof(*data)); return 0; } @@ -448,73 +430,22 @@ if 'EXTENDED_STRUCT_MEMBER' not in x) if args.full_path: - bpf_text = bpf_text.replace('SUBMIT_DATA', """ + bpf_text = bpf_text.replace('GET_FULL_PATH', """ if (data->name[0] != '/') { // relative path - struct task_struct *task; - struct dentry *dentry, *parent_dentry, *mnt_root; - struct vfsmount *vfsmnt; - struct fs_struct *fs; - struct path *path; - struct mount *mnt; - size_t filepart_length; - char *payload = data->name; - struct qstr d_name; - int i; - - task = (struct task_struct *)bpf_get_current_task_btf(); - - fs = task->fs; - path = &fs->pwd; - dentry = path->dentry; - vfsmnt = path->mnt; - - mnt = container_of(vfsmnt, struct mount, mnt); - - for (i = 1, payload += NAME_MAX; i < MAX_ENTRIES; i++) { - bpf_probe_read_kernel(&d_name, sizeof(d_name), &dentry->d_name); - filepart_length = - bpf_probe_read_kernel_str(payload, NAME_MAX, (void *)d_name.name); - - if (filepart_length < 0 || filepart_length > NAME_MAX) - break; - - bpf_probe_read_kernel(&mnt_root, sizeof(mnt_root), &vfsmnt->mnt_root); - bpf_probe_read_kernel(&parent_dentry, sizeof(parent_dentry), &dentry->d_parent); - - if (dentry == parent_dentry || dentry == mnt_root) { - struct mount *mnt_parent; - bpf_probe_read_kernel(&mnt_parent, sizeof(mnt_parent), &mnt->mnt_parent); - - if (mnt != mnt_parent) { - bpf_probe_read_kernel(&dentry, sizeof(dentry), &mnt->mnt_mountpoint); - - mnt = mnt_parent; - vfsmnt = &mnt->mnt; - - bpf_probe_read_kernel(&mnt_root, sizeof(mnt_root), &vfsmnt->mnt_root); - - data->path_depth++; - payload += NAME_MAX; - continue; - } else { - /* Real root directory */ - break; - } - } - - payload += NAME_MAX; - - dentry = parent_dentry; - data->path_depth++; - } + bpf_getcwd(data->name + NAME_MAX, NAME_MAX, MAX_ENTRIES - 1, + &data->path_depth); } - - events.ringbuf_submit(data, sizeof(*data)); """) + + with open(BPF._find_file("full_path.h".encode("utf-8"))) as fileobj: + progtxt = fileobj.read() + bpf_text = bpf_text.replace('INCLUDE_FULL_PATH_H', progtxt) + + with open(BPF._find_file("path_helpers.bpf.c".encode("utf-8"))) as fileobj: + progtxt = fileobj.read() + bpf_text = bpf_text.replace('INCLUDE_PATH_HELPERS_BPF_H', progtxt) else: - bpf_text = bpf_text.replace('SUBMIT_DATA', """ - events.ringbuf_submit(data, sizeof(*data)); - """) + bpf_text = bpf_text.replace('GET_FULL_PATH', """""") if debug or args.ebpf: print(bpf_text) @@ -552,12 +483,6 @@ entries = defaultdict(list) -def split_names(str): - NAME_MAX = 255 - MAX_ENTRIES = 32 - chunks = [str[i:i + NAME_MAX] for i in range(0, NAME_MAX * MAX_ENTRIES, NAME_MAX)] - return [chunk.split(b'\x00', 1)[0] for chunk in chunks] - # process event def print_event(cpu, data, size): event = b["events"].event(data) @@ -604,17 +529,10 @@ def print_event(cpu, data, size): printb(b"%08o %04o " % (event.flags, event.mode), nl="") if args.full_path: - # see struct data_t::name field comment. - names = split_names(bytes(event.name)) - picked = names[:event.path_depth + 1] - picked_str = [] - for x in picked: - s = x.decode('utf-8', 'ignore') if isinstance(x, bytes) else str(x) - # remove mountpoint '/' and empty string - if s != "/" and s != "": - picked_str.append(s) - joined = '/'.join(picked_str[::-1]) - result = joined if joined.startswith('/') else '/' + joined + import sys + sys.path.append(os.path.dirname(sys.argv[0])) + from path_helpers import get_full_path + result = get_full_path(event.name, event.path_depth) printb(b"%s" % result.encode("utf-8")) else: printb(b"%s" % event.name) diff --git a/tools/path_helpers.bpf.c b/tools/path_helpers.bpf.c new file mode 100755 index 000000000000..880ca02da6ad --- /dev/null +++ b/tools/path_helpers.bpf.c @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +/* Copyright (c) 2025 Rong Tao */ +#ifndef __PATH_HELPERS_BPF_H +#define __PATH_HELPERS_BPF_H 1 + +#include +#include +#include +#include + +/* see https://github.com/torvalds/linux/blob/master/fs/mount.h */ +struct mount { + struct hlist_node mnt_hash; + struct mount *mnt_parent; + struct dentry *mnt_mountpoint; + struct vfsmount mnt; + /* ... */ +}; + + +static __always_inline +int bpf_dentry_full_path(char *pathes, int name_len, int max_depth, + struct dentry *dentry, struct vfsmount *vfsmnt, + __u32 *path_depth) +{ + struct dentry *parent_dentry, *mnt_root; + struct mount *mnt; + size_t filepart_length; + char *payload = pathes; + struct qstr d_name; + int i; + + mnt = container_of(vfsmnt, struct mount, mnt); + + for (i = 1, payload += name_len; i < max_depth; i++) { + bpf_probe_read_kernel(&d_name, sizeof(d_name), &dentry->d_name); + filepart_length = + bpf_probe_read_kernel_str(payload, name_len, (void *)d_name.name); + + if (filepart_length < 0 || filepart_length > name_len) + break; + + bpf_probe_read_kernel(&mnt_root, sizeof(mnt_root), &vfsmnt->mnt_root); + bpf_probe_read_kernel(&parent_dentry, sizeof(parent_dentry), &dentry->d_parent); + + if (dentry == parent_dentry || dentry == mnt_root) { + struct mount *mnt_parent; + bpf_probe_read_kernel(&mnt_parent, sizeof(mnt_parent), &mnt->mnt_parent); + + if (mnt != mnt_parent) { + bpf_probe_read_kernel(&dentry, sizeof(dentry), &mnt->mnt_mountpoint); + + mnt = mnt_parent; + vfsmnt = &mnt->mnt; + + bpf_probe_read_kernel(&mnt_root, sizeof(mnt_root), &vfsmnt->mnt_root); + + (*path_depth)++; + payload += name_len; + continue; + } else { + /* Real root directory */ + break; + } + } + + payload += name_len; + + dentry = parent_dentry; + (*path_depth)++; + } + + return 0; +} + +static __always_inline +int bpf_getcwd(char *pathes, int name_len, int max_depth, __u32 *path_depth) +{ + struct task_struct *task; + struct fs_struct *fs; + struct dentry *dentry; + struct vfsmount *vfsmnt; + + task = (struct task_struct *)bpf_get_current_task_btf(); + bpf_probe_read_kernel(&fs, sizeof(fs), &task->fs); + bpf_probe_read_kernel(&dentry, sizeof(dentry), &fs->pwd.dentry); + bpf_probe_read_kernel(&vfsmnt, sizeof(vfsmnt), &fs->pwd.mnt); + + return bpf_dentry_full_path(pathes, name_len, max_depth, dentry, vfsmnt, + path_depth); +} +#endif diff --git a/tools/path_helpers.py b/tools/path_helpers.py new file mode 100644 index 000000000000..9b73dbf33f1c --- /dev/null +++ b/tools/path_helpers.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# Path related helper functions +# +# Copyright (c) 2025 Rong Tao +# Licensed under the Apache License, Version 2.0 (the "License") +# +# 30-Jun-2025 Rong Tao Created this. + +import os + +def full_path_split_names(str, name_max=255, max_entries=32): + name_max = 255 + max_entries = 32 + chunks = [str[i:i + name_max] for i in range(0, name_max * max_entries, name_max)] + return [chunk.split(b'\x00', 1)[0] for chunk in chunks] + + +# parse full-path, see tools/full_path.h +def get_full_path(name, depth, name_max=255, max_entries=32): + names = full_path_split_names(bytes(name), name_max, max_entries) + picked = names[:depth + 1] + picked_str = [] + for x in picked: + s = x.decode('utf-8', 'ignore') if isinstance(x, bytes) else str(x) + # remove mountpoint '/' and empty string + if s != "/" and s != "": + picked_str.append(s) + joined = '/'.join(picked_str[::-1]) + result = joined if joined.startswith('/') else '/' + joined + return result