From 6dde07e380d2138622b92be587ff1a7a0a6180f9 Mon Sep 17 00:00:00 2001 From: Rong Tao Date: Tue, 29 Jul 2025 19:45:59 +0800 Subject: [PATCH 1/6] tools: Introduce path_helpers Add the path_helpers code and header file. These functions are separate from opensnoop.py, and because the code changes are large, this commit does not modify the opensnoop.py code. Add source code: - full_path.h: defined FULL_PATH_FIELD(name); - path_helpers.bpf.c: add bpf_dentry_full_path() and bpf_getcwd() helpers; - path_helpers.py: add get_full_path() to parse full-path in full_path.h; Signed-off-by: Rong Tao --- tools/CMakeLists.txt | 9 +++- tools/full_path.h | 22 ++++++++++ tools/path_helpers.bpf.c | 92 ++++++++++++++++++++++++++++++++++++++++ tools/path_helpers.py | 30 +++++++++++++ 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tools/full_path.h create mode 100755 tools/path_helpers.bpf.c create mode 100644 tools/path_helpers.py 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/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/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 From 7ea5f9f199caa7564abf26512faebcf453e62639 Mon Sep 17 00:00:00 2001 From: Rong Tao Date: Tue, 29 Jul 2025 20:03:11 +0800 Subject: [PATCH 2/6] tools/opensnoop: Apply path_helpers Apply path_helpers to opensnoop. Signed-off-by: Rong Tao --- tools/opensnoop.py | 132 +++++++++------------------------------------ 1 file changed, 25 insertions(+), 107 deletions(-) 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) From e25eb158ae4c330949cf1d07526f205044f9568e Mon Sep 17 00:00:00 2001 From: Rong Tao Date: Tue, 29 Jul 2025 20:31:04 +0800 Subject: [PATCH 3/6] tools/filelife: Use ring-buffer instead of perf-buffer In order for filelife to support file paths, it is necessary to replace perf-buffer with ring-buffer, because the single event size of path information transmission is large, and it is impossible to statically allocate events in the stack, so it is necessary to use ring-buffer reservation mechanism. At the same time, 'create_arg' and 'unlink_event' structures are separated from 'data_t' for file creation and deletion events and data record transfer. Signed-off-by: Rong Tao --- tools/filelife.py | 76 +++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/tools/filelife.py b/tools/filelife.py index 1f00dbdcd7e9..df94e7e25520 100755 --- a/tools/filelife.py +++ b/tools/filelife.py @@ -51,21 +51,33 @@ u64 delta; char comm[TASK_COMM_LEN]; char fname[DNAME_INLINE_LEN]; - /* private */ - void *dentry; }; -BPF_HASH(birth, struct dentry *); -BPF_HASH(unlink_data, u32, struct data_t); -BPF_PERF_OUTPUT(events); +struct create_arg { + u64 ts; +}; + +struct unlink_event { + u32 tid; + u64 delta; + struct dentry *dentry; +}; + +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 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; + + birth.update(&dentry, &arg); return 0; } @@ -98,46 +110,40 @@ // 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; - 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 +153,20 @@ 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)); + + struct qstr d_name = unlink_event->dentry->d_name; + bpf_probe_read_kernel_str(&data->fname, sizeof(data->fname), d_name.name); + + birth.delete((struct dentry **)&unlink_event->dentry); + + events.ringbuf_submit(data, sizeof(*data)); return 0; } @@ -220,9 +238,9 @@ def print_event(cpu, data, size): event.comm.decode('utf-8', 'replace'), float(event.delta) / 1000, event.fname.decode('utf-8', 'replace'))) -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() From bc59c6091cd5a8d3bb98637ca560748c882ab9a9 Mon Sep 17 00:00:00 2001 From: Rong Tao Date: Tue, 29 Jul 2025 20:48:59 +0800 Subject: [PATCH 4/6] tools/filelife: Support file path Support for file paths using path_helpers. For example: $ sudo ./filelife.py -P TIME PID COMM AGE(s) FILE 20:51:32 55738 rm 0.21 /home/sdb/Git/bcc/build/a.out 20:51:44 47715 Chrome_ChildIOT 0.00 /home/sdb/.org.chromium.Chromium.3hn6CS 20:51:44 3490 ThreadPoolForeg 10.00 /home/rongtao/.cache/google-chrome/Default/Cache/Cache_Data/todelete_8829186fc5f5441a_0_1 20:51:49 3490 ThreadPoolForeg 10.00 /home/rongtao/.cache/google-chrome/Default/Cache/Cache_Data/todelete_25ef4b49ebd6a803_0_1 20:51:49 55767 rm 6.42 /home/sdb/Git/bcc/build/a.out Signed-off-by: Rong Tao --- tools/filelife.py | 70 ++++++++++++++++++++++++++++++++++---- tools/filelife_example.txt | 14 ++++---- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/tools/filelife.py b/tools/filelife.py index df94e7e25520..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,22 +50,34 @@ #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]; +#endif }; 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); @@ -69,6 +86,8 @@ 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 @@ -76,6 +95,11 @@ u64 ts = bpf_ktime_get_ns(); 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); @@ -130,6 +154,7 @@ event.delta = delta; event.tid = tid; event.dentry = dentry; + event.cwd_vfsmnt = arg->cwd_vfsmnt; unlink_data.update(&tid, &event); return 0; @@ -161,8 +186,18 @@ 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); @@ -201,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) @@ -216,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") @@ -234,9 +282,17 @@ # 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'))) + 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: 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 From 80f44ebb688f2c26302f102c6687debe55b4e3e8 Mon Sep 17 00:00:00 2001 From: Rong Tao Date: Tue, 29 Jul 2025 21:04:29 +0800 Subject: [PATCH 5/6] tools/filegone: Use ring-buffer instead of perf-buffer In order for filegone to support file paths, it is necessary to replace perf-buffer with ring-buffer, because the single event size of path information transmission is large, and it is impossible to statically allocate events in the stack, so it is necessary to use ring-buffer reservation mechanism. Add 'struct entry_t' to pass the information from kprobe to kretprobe. Signed-off-by: Rong Tao --- tools/filegone.py | 96 ++++++++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/tools/filegone.py b/tools/filegone.py index 9b8c01684a19..9ab013871ae2 100755 --- a/tools/filegone.py +++ b/tools/filegone.py @@ -47,8 +47,24 @@ char fname2[DNAME_INLINE_LEN]; }; -BPF_PERF_OUTPUT(events); -BPF_HASH(currdata, u32, struct data_t); +struct entry_t { + u32 pid; + u8 action; + struct { + char name[DNAME_INLINE_LEN]; + } 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 +75,13 @@ 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'; + get_dentry_name((char **)&entry.old.name, dentry); - currdata.update(&tid, &data); + currentry.update(&tid, &entry); return 0; } @@ -77,24 +89,24 @@ // 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. + */ + get_dentry_name((char **)&entry.old.name, old_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 +114,34 @@ 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); + + if (entry->action == 'R') + bpf_probe_read(&data->fname2, sizeof(data->fname2), entry->new.name); + + events.ringbuf_submit(data, sizeof(*data)); return 0; } """ @@ -155,11 +181,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 +192,12 @@ 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) +# 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") @@ -189,13 +216,14 @@ def print_event(cpu, data, size): 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')) + 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() From 704c8797ef3080e2f0c057dda6ae424564103a02 Mon Sep 17 00:00:00 2001 From: Rong Tao Date: Tue, 29 Jul 2025 21:23:06 +0800 Subject: [PATCH 6/6] tools/filegone: Support file path Support for file paths using path_helpers. For example: $ realpath . /home/sdb/Git/bcc/build $ touch a.out && sleep 0.2 && mv a.out b.out && sleep 0.2 && rm b.out $ sudo ./filegone.py -P TIME PID COMM ACTION FILE 21:22:37 58683 mv RENAME /home/sdb/Git/bcc/build/a.out > /home/sdb/Git/bcc/build/b.out 21:22:37 58685 rm DELETE /home/sdb/Git/bcc/build/b.out Signed-off-by: Rong Tao --- tools/filegone.py | 76 +++++++++++++++++++++++++++++++++++--- tools/filegone_example.txt | 11 ++++-- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/tools/filegone.py b/tools/filegone.py index 9ab013871ae2..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,14 +41,25 @@ 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 }; struct entry_t { @@ -52,6 +67,7 @@ u8 action; struct { char name[DNAME_INLINE_LEN]; + struct dentry *dentry; } old, new; }; @@ -79,6 +95,7 @@ entry.pid = pid; entry.action = 'D'; + entry.old.dentry = dentry; get_dentry_name((char **)&entry.old.name, dentry); currentry.update(&tid, &entry); @@ -103,7 +120,9 @@ * 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); currentry.update(&tid, &entry); @@ -138,9 +157,33 @@ 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; } @@ -192,6 +235,17 @@ 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) @@ -214,10 +268,20 @@ 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": - file2_str = event.fname2.decode('utf-8', 'replace') - file_str = "%s > %s" % (file_str, file2_str) + 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)) 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