From e673c050abdc26b08a1fc1876524bcdebcbd4744 Mon Sep 17 00:00:00 2001 From: sparkzky Date: Wed, 22 Jul 2026 02:01:03 +0800 Subject: [PATCH 1/6] feat(tracepoint): add sched_process_exec tracepoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 execve 成功路径添加 sched_process_exec tracepoint,供 ANOLISA agentsight 的 eBPF 程序追踪进程 exec 事件、构建 AI agent 进程树。 - 新增 kernel/src/process/trace.rs:声明 sched_process_exec tracepoint(comm/pid/old_pid 字段),TP_system(sched) - execve.rs:在 load_binary_file_with_context 之前捕获 old_pid(de_thread 会交换 pid);trace 调用置于 arch_do_execve 成功后、is_ok() 分支内,对齐 Linux fs/exec.c:1803 - mod.rs:注册 process::trace 模块 tracepoint 注册、debugfs 导出、eBPF attach 全部由现有框架自动完成。static key 保证未启用时零开销。 Refs: #2149 Signed-off-by: sparkzky --- kernel/src/process/execve.rs | 11 +++++++++++ kernel/src/process/mod.rs | 1 + kernel/src/process/trace.rs | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 kernel/src/process/trace.rs diff --git a/kernel/src/process/execve.rs b/kernel/src/process/execve.rs index 7827aec251..9346a35e58 100644 --- a/kernel/src/process/execve.rs +++ b/kernel/src/process/execve.rs @@ -1,3 +1,4 @@ +use super::trace::trace_sched_process_exec; use crate::arch::CurrentIrqArch; use crate::exception::InterruptArch; use crate::filesystem::vfs::fcntl::AtFlags; @@ -128,6 +129,11 @@ fn do_execve_internal( let old_vm = do_execve_switch_user_vm(address_space.clone()); + // 捕获 sched_process_exec 的 old_pid:必须在 load_binary_file_with_context 之前, + // 因为该函数内的 begin_new_exec → de_thread 会在「非 leader 线程 execve」时 + // 交换 current 与旧 thread-group leader 的 raw_pid(对齐 Linux fs/exec.c:1770)。 + let old_pid = ProcessManager::current_pcb().raw_pid().data() as i32; + // 尝试加载二进制文件 let load_result = load_binary_file_with_context(&mut param, &ctx); @@ -214,6 +220,11 @@ fn do_execve_internal( let vfork_done = pcb.thread.write_irqsave().vfork_done.take(); let exec_ret = Syscall::arch_do_execve(regs, ¶m, &result, user_sp, argv_ptr); if exec_ret.is_ok() { + // sched_process_exec:arch_do_execve 成功、用户态寄存器就绪后触发, + // 对齐 Linux fs/exec.c:1803(trace 在 start_thread 之后、所有失败点之后)。 + let pid = pcb.raw_pid().data() as i32; + trace_sched_process_exec(pcb.basic().name(), pid, old_pid); + if let Some(completion) = vfork_done { completion.complete_all(); } diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 847ab86c36..31c7df7ca1 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -24,6 +24,7 @@ pub mod signal; pub mod stdio; pub mod syscall; pub mod timer; +pub mod trace; pub mod utils; pub mod wait; diff --git a/kernel/src/process/trace.rs b/kernel/src/process/trace.rs new file mode 100644 index 0000000000..718b2d9e80 --- /dev/null +++ b/kernel/src/process/trace.rs @@ -0,0 +1,36 @@ +//! 进程/调度类 tracepoint 声明。 +//! +//! 字段参考 Linux `include/trace/events/sched.h`。 +//! 注意:DragonOS 的 `define_event_trace!` 宏不支持 Linux 的 `__string` 动态字符串, +//! 且当前未实现 `bpf_get_current_comm()` helper,故 `comm` 直接放入 payload。 + +use crate::define_event_trace; + +define_event_trace!( + sched_process_exec, + TP_system(sched), + TP_PROTO(comm: &str, pid: i32, old_pid: i32), + TP_STRUCT__entry { + comm: [u8; 16], + pid: i32, + old_pid: i32, + }, + TP_fast_assign { + comm: { + // 对齐 Linux TASK_COMM_LEN=16(15 字符 + NUL)。 + let mut buf = [0u8; 16]; + let bytes = comm.as_bytes(); + let len = bytes.len().min(15); + buf[..len].copy_from_slice(&bytes[..len]); + buf + }, + pid: pid, + old_pid: old_pid, + }, + TP_ident(__entry), + TP_printk({ + let nul = __entry.comm.iter().position(|&b| b == 0).unwrap_or(16); + let comm = core::str::from_utf8(&__entry.comm[..nul]).unwrap_or("invalid utf8"); + format!("comm={} pid={} old_pid={}", comm, __entry.pid, __entry.old_pid) + }) +); From 5b1c50988b3102a5e312412092251df4502df695 Mon Sep 17 00:00:00 2001 From: sparkzky Date: Thu, 23 Jul 2026 01:32:27 +0800 Subject: [PATCH 2/6] test(dunitest): add sched_process_exec tracepoint test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 sched_tracepoint dunitest,验证 sched_process_exec tracepoint 的 debugfs 导出(format/enable/id 文件 + 字段)与 execve 触发行为。 - EventFilesExist: 验证 events/sched/sched_process_exec/{format,id,enable} 存在,format 含 comm/pid/old_pid/common_pid 字段 - FiresOnExecve: enable + 清空 trace 后 fork+execve(/proc/self/exe),断言 trace 含 sched_process_exec 记录与 comm 字段 - whitelist 注册 normal/sched_tracepoint Refs: #2149 Signed-off-by: sparkzky --- .../suites/normal/sched_tracepoint.cc | 187 ++++++++++++++++++ user/apps/tests/dunitest/whitelist.txt | 1 + 2 files changed, 188 insertions(+) create mode 100644 user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc diff --git a/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc b/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc new file mode 100644 index 0000000000..6b971416b7 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc @@ -0,0 +1,187 @@ +// sched_process_exec tracepoint 语义测试。 +// +// 验证: +// 1. sched_process_exec 事件在 debugfs 下正确导出(enable/format/id 文件 + format 字段)。 +// 2. enable 后执行 execve 会触发该 tracepoint,并在 trace 文件中留下记录。 +// +// 对应 issue #2149。 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +constexpr char kHelperExec[] = "--sched-tp-helper-exit0"; + +// 读 path 全部内容(非阻塞,读到 EOF 为止)。 +std::string read_all(const char* path) { + int fd = open(path, O_RDONLY); + EXPECT_GE(fd, 0) << "open(" << path << ") failed: errno=" << errno << " (" + << strerror(errno) << ")"; + if (fd < 0) { + return {}; + } + std::string out; + char buf[256]; + while (true) { + ssize_t n = read(fd, buf, sizeof(buf)); + if (n <= 0) { + break; + } + out.append(buf, static_cast(n)); + } + close(fd); + return out; +} + +// 向 path 写 data,返回是否写成功。 +bool write_file(const char* path, const char* data) { + int fd = open(path, O_WRONLY); + if (fd < 0) { + return false; + } + size_t len = strlen(data); + ssize_t n = write(fd, data, len); + close(fd); + return n == static_cast(len); +} + +// mount debugfs 到 root。 +void mount_debugfs(const char* root) { + ASSERT_EQ(0, mount("none", root, "debugfs", 0, nullptr)) + << "mount debugfs failed: errno=" << errno << " (" << strerror(errno) << ")"; +} + +// 子模式:被 execve 进来后立即退出 0。 +[[noreturn]] void helper_exec_exit0() { + char arg0[] = "/proc/self/exe"; + char arg1[] = "--sched-tp-helper-exit0"; + char* const argv[] = {arg0, arg1, nullptr}; + char* const envp[] = {nullptr}; + execve("/proc/self/exe", argv, envp); + _exit(127); +} + +} // namespace + +// 事件文件存在且 format 含全部字段。 +TEST(SchedProcessExecTp, EventFilesExist) { + char root[128] = {}; + snprintf(root, sizeof(root), "/tmp/sched_tp_events_%d", getpid()); + ASSERT_EQ(0, mkdir(root, 0755)) << strerror(errno); + + mount_debugfs(root); + + const char* base_rel = "/tracing/events/sched/sched_process_exec"; + char base[256] = {}; + snprintf(base, sizeof(base), "%s%s", root, base_rel); + + struct stat st = {}; + ASSERT_EQ(0, stat(base, &st)) << "missing event dir " << base << ": " << strerror(errno); + EXPECT_TRUE(S_ISDIR(st.st_mode)); + + char file[320] = {}; + for (const char* leaf : {"enable", "format", "id"}) { + snprintf(file, sizeof(file), "%s/%s", base, leaf); + ASSERT_EQ(0, stat(file, &st)) + << "missing " << file << ": " << strerror(errno); + } + + // format 文件应含事件名与全部字段。 + snprintf(file, sizeof(file), "%s/format", base); + std::string fmt = read_all(file); + ASSERT_FALSE(fmt.empty()); + for (const char* needle : + {"sched_process_exec", "common_pid", "comm", "pid", "old_pid"}) { + EXPECT_NE(std::string::npos, fmt.find(needle)) + << "format missing \"" << needle << "\"\n" + << fmt; + } + + // enable 默认为 "0"(未启用)。 + snprintf(file, sizeof(file), "%s/enable", base); + std::string enable = read_all(file); + EXPECT_NE(std::string::npos, enable.find("0")) << "enable not '0' by default: " << enable; + + // id 应为数字。 + snprintf(file, sizeof(file), "%s/id", base); + std::string id = read_all(file); + EXPECT_FALSE(id.empty()); + char* end = nullptr; + long idval = strtol(id.c_str(), &end, 10); + EXPECT_GT(idval, 0) << "invalid id: " << id; + + EXPECT_EQ(0, umount(root)) << strerror(errno); + EXPECT_EQ(0, rmdir(root)) << strerror(errno); +} + +// enable 后 execve 应触发事件,trace 文件留下记录。 +TEST(SchedProcessExecTp, FiresOnExecve) { + char root[128] = {}; + snprintf(root, sizeof(root), "/tmp/sched_tp_fire_%d", getpid()); + ASSERT_EQ(0, mkdir(root, 0755)) << strerror(errno); + + mount_debugfs(root); + + const char* base_rel = "/tracing/events/sched/sched_process_exec"; + char base[256] = {}; + snprintf(base, sizeof(base), "%s%s", root, base_rel); + + // 启用事件。 + char enable_path[320] = {}; + snprintf(enable_path, sizeof(enable_path), "%s/enable", base); + ASSERT_TRUE(write_file(enable_path, "1")) << "enable write failed"; + + // 清空 ring buffer:向 trace 写任意字节触发 clear。 + char trace_path[256] = {}; + snprintf(trace_path, sizeof(trace_path), "%s/tracing/trace", root); + ASSERT_TRUE(write_file(trace_path, "1")) << "trace clear write failed"; + + // fork + execve 自身触发 sched_process_exec。 + pid_t child = fork(); + ASSERT_GE(child, 0) << "fork failed: " << strerror(errno); + if (child == 0) { + helper_exec_exit0(); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << "waitpid failed: " << strerror(errno); + ASSERT_TRUE(WIFEXITED(status)) << "child did not exit normally, status=" << status; + EXPECT_EQ(0, WEXITSTATUS(status)) << "helper exit code != 0"; + + // 读 trace 快照,断言含 sched_process_exec 记录。 + std::string trace = read_all(trace_path); + ASSERT_FALSE(trace.empty()) << "trace empty after execve"; + EXPECT_NE(std::string::npos, trace.find("sched_process_exec(")) + << "no sched_process_exec record in trace:\n" + << trace; + // TP_printk 输出的字段。 + EXPECT_NE(std::string::npos, trace.find("comm=")) + << "trace missing comm= field:\n" + << trace; + + // 关闭事件并清理。 + write_file(enable_path, "0"); + EXPECT_EQ(0, umount(root)) << strerror(errno); + EXPECT_EQ(0, rmdir(root)) << strerror(errno); +} + +int main(int argc, char** argv) { + // 子模式:被 execve 进来后立即退出 0。 + if (argc >= 2 && strcmp(argv[1], kHelperExec) == 0) { + _exit(0); + } + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index fbfc9c3af8..4143c09797 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -85,3 +85,4 @@ normal/virtiofs_dax normal/af_packet_sockopt normal/af_packet_e2e normal/af_packet_mcast +normal/sched_tracepoint From 0cff71c7fe179833a8f2e57111d0d4fdd380af99 Mon Sep 17 00:00:00 2001 From: sparkzky Date: Thu, 23 Jul 2026 01:55:46 +0800 Subject: [PATCH 3/6] fix(test): sched_tracepoint id assertion - id starts from 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TracePointIdFile 的 id 由 global_init_events 的 AtomicUsize::new(0) fetch_add 分配,从 0 开始递增。原断言 EXPECT_GT(idval, 0) 错误假设 id 从 1 开始,导致 CI 中拿到 id=0 的 tracepoint 失败。改为 EXPECT_GE(idval, 0)。 Refs: #2149 Signed-off-by: sparkzky --- user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc b/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc index 6b971416b7..33fbe51a9d 100644 --- a/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc +++ b/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc @@ -114,13 +114,13 @@ TEST(SchedProcessExecTp, EventFilesExist) { std::string enable = read_all(file); EXPECT_NE(std::string::npos, enable.find("0")) << "enable not '0' by default: " << enable; - // id 应为数字。 + // id 应为非负整数(DragonOS tracepoint id 从 0 开始递增分配)。 snprintf(file, sizeof(file), "%s/id", base); std::string id = read_all(file); EXPECT_FALSE(id.empty()); char* end = nullptr; long idval = strtol(id.c_str(), &end, 10); - EXPECT_GT(idval, 0) << "invalid id: " << id; + EXPECT_GE(idval, 0) << "invalid id: " << id; EXPECT_EQ(0, umount(root)) << strerror(errno); EXPECT_EQ(0, rmdir(root)) << strerror(errno); From 72222fff1943150616f18729d4f96284220b9aeb Mon Sep 17 00:00:00 2001 From: sparkzky Date: Thu, 23 Jul 2026 23:45:56 +0800 Subject: [PATCH 4/6] fix(execve): release basic read lock before triggering sched_process_exec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR review 发现:trace_sched_process_exec(pcb.basic().name(), ...) 持有 basic 读锁 guard 直到 trace 调用返回,而 trace 默认回调内部的 trace_cmdline_push() 会再次获取同一把 basic 读锁。DragonOS RwLock 读锁不可重入,若此时另一 CPU 排队写锁,将导致 reader 等 writer、writer 等 reader 的 deadlock。 修复:先将 comm 复制到栈缓冲并在内层作用域释放读锁 guard,再调用 trace,消除锁重入。 Refs: #2149 Signed-off-by: sparkzky --- kernel/src/process/execve.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/kernel/src/process/execve.rs b/kernel/src/process/execve.rs index 9346a35e58..fda8c25885 100644 --- a/kernel/src/process/execve.rs +++ b/kernel/src/process/execve.rs @@ -223,7 +223,18 @@ fn do_execve_internal( // sched_process_exec:arch_do_execve 成功、用户态寄存器就绪后触发, // 对齐 Linux fs/exec.c:1803(trace 在 start_thread 之后、所有失败点之后)。 let pid = pcb.raw_pid().data() as i32; - trace_sched_process_exec(pcb.basic().name(), pid, old_pid); + // 先把 comm 复制到栈缓冲并释放 basic 读锁:trace 默认回调内部的 + // trace_cmdline_push 会再次获取 basic 读锁,持锁重入有 deadlock 风险。 + let mut comm_buf = [0u8; 16]; + let comm_len; + { + let basic_guard = pcb.basic(); + let name = basic_guard.name(); + comm_len = name.as_bytes().len().min(15); + comm_buf[..comm_len].copy_from_slice(&name.as_bytes()[..comm_len]); + } + let comm = core::str::from_utf8(&comm_buf[..comm_len]).unwrap_or(""); + trace_sched_process_exec(comm, pid, old_pid); if let Some(completion) = vfork_done { completion.complete_all(); From a9e1305a406fcda68aa0a88b4b4da7aea7497dba Mon Sep 17 00:00:00 2001 From: sparkzky Date: Fri, 24 Jul 2026 00:31:05 +0800 Subject: [PATCH 5/6] fix(execve): avoid splitting multi-byte UTF-8 char in comm truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bug-hunter fanout 发现:comm_len = bytes.len().min(15) 在原始字节偏移截断,当进程名第 15 字节落在多字节 UTF-8 字符中间时(如 Unicode 路径名),from_utf8 失败导致 comm 变空字符串。用 is_char_boundary 回退到字符边界修复。 同时加强测试:strtol 解析 id 后校验 end 指针,确认整个字符串都是数字而非仅前缀可解析。 Refs: #2149 Signed-off-by: sparkzky --- kernel/src/process/execve.rs | 6 +++++- user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/kernel/src/process/execve.rs b/kernel/src/process/execve.rs index fda8c25885..0f83a071bf 100644 --- a/kernel/src/process/execve.rs +++ b/kernel/src/process/execve.rs @@ -226,11 +226,15 @@ fn do_execve_internal( // 先把 comm 复制到栈缓冲并释放 basic 读锁:trace 默认回调内部的 // trace_cmdline_push 会再次获取 basic 读锁,持锁重入有 deadlock 风险。 let mut comm_buf = [0u8; 16]; - let comm_len; + let mut comm_len; { let basic_guard = pcb.basic(); let name = basic_guard.name(); comm_len = name.as_bytes().len().min(15); + // 回退到 UTF-8 字符边界,避免在多字节字符中间截断导致 from_utf8 失败。 + while comm_len > 0 && !name.is_char_boundary(comm_len) { + comm_len -= 1; + } comm_buf[..comm_len].copy_from_slice(&name.as_bytes()[..comm_len]); } let comm = core::str::from_utf8(&comm_buf[..comm_len]).unwrap_or(""); diff --git a/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc b/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc index 33fbe51a9d..1380521982 100644 --- a/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc +++ b/user/apps/tests/dunitest/suites/normal/sched_tracepoint.cc @@ -120,6 +120,10 @@ TEST(SchedProcessExecTp, EventFilesExist) { EXPECT_FALSE(id.empty()); char* end = nullptr; long idval = strtol(id.c_str(), &end, 10); + // 确认整个字符串都是数字(允许尾部换行),而非仅前缀可解析。 + ASSERT_NE(end, id.c_str()) << "id not numeric: " << id; + while (end != nullptr && (*end == '\n' || *end == '\r' || *end == ' ')) ++end; + EXPECT_EQ(end != nullptr && *end == '\0', true) << "id has trailing garbage: " << id; EXPECT_GE(idval, 0) << "invalid id: " << id; EXPECT_EQ(0, umount(root)) << strerror(errno); From 5721983618156a7609832c2d7d764cf70d8eb03f Mon Sep 17 00:00:00 2001 From: sparkzky Date: Fri, 24 Jul 2026 01:31:32 +0800 Subject: [PATCH 6/6] style(execve): use name.len() per clippy needless_as_bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make fmt / clippy: name.as_bytes().len() → name.len()(字符串可直接调 len())。 Refs: #2149 Signed-off-by: sparkzky --- kernel/src/process/execve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/src/process/execve.rs b/kernel/src/process/execve.rs index 0f83a071bf..0cc93ed8fe 100644 --- a/kernel/src/process/execve.rs +++ b/kernel/src/process/execve.rs @@ -230,7 +230,7 @@ fn do_execve_internal( { let basic_guard = pcb.basic(); let name = basic_guard.name(); - comm_len = name.as_bytes().len().min(15); + comm_len = name.len().min(15); // 回退到 UTF-8 字符边界,避免在多字节字符中间截断导致 from_utf8 失败。 while comm_len > 0 && !name.is_char_boundary(comm_len) { comm_len -= 1;