Linux内核trace_sched_process_exec钩子注册示例
Linux内核trace_sched_process_exec钩子注册示例
sched_process_exec
是一个Linux内核跟踪点(tracepoint),用于在进程执行execve()系统调用时触发。以下是如何注册和使用这个tracepoint的示例。
1. 使用tracepoint_notifier机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <linux/module.h> #include <linux/kernel.h> #include <linux/trace_events.h> #include <linux/sched.h>
static void tp_sched_process_exec(void *ignore, struct task_struct *task, pid_t old_pid, struct linux_binprm *bprm) { char filename[256]; char *pathname = d_path(&bprm->file->f_path, filename, sizeof(filename)); printk(KERN_INFO "exec: pid=%d (%s -> %s) path=%s\n", task->pid, task->comm, bprm->filename, pathname); }
static int __init trace_init(void) { int ret; ret = register_trace_sched_process_exec(tp_sched_process_exec, NULL); if (ret) { printk(KERN_ERR "Failed to register tracepoint\n"); return ret; } printk(KERN_INFO "sched_process_exec tracepoint registered\n"); return 0; }
static void __exit trace_exit(void) { unregister_trace_sched_process_exec(tp_sched_process_exec, NULL); printk(KERN_INFO "sched_process_exec tracepoint unregistered\n"); }
module_init(trace_init); module_exit(trace_exit); MODULE_LICENSE("GPL");
|
2. 使用ftrace事件机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| #include <linux/module.h> #include <linux/kernel.h> #include <linux/trace_events.h>
static struct trace_event_call *event_call;
static notrace void probe_sched_process_exec(void *ignore, struct task_struct *task, pid_t old_pid, struct linux_binprm *bprm) { char filename[256]; char *pathname = d_path(&bprm->file->f_path, filename, sizeof(filename)); trace_printk("exec: pid=%d (%s -> %s) path=%s\n", task->pid, task->comm, bprm->filename, pathname); }
static int __init trace_init(void) { int ret; event_call = trace_get_event_call("sched", "sched_process_exec"); if (!event_call) { printk(KERN_ERR "Failed to get sched_process_exec event\n"); return -ENODEV; } ret = trace_event_probe_register(event_call, probe_sched_process_exec, NULL); if (ret) { printk(KERN_ERR "Failed to register probe\n"); return ret; } printk(KERN_INFO "sched_process_exec probe registered\n"); return 0; }
static void __exit trace_exit(void) { if (event_call) trace_event_probe_unregister(event_call, probe_sched_process_exec, NULL); printk(KERN_INFO "sched_process_exec probe unregistered\n"); }
module_init(trace_init); module_exit(trace_exit); MODULE_LICENSE("GPL");
|
3. 使用BPF (eBPF) 方式
如果你使用的是Linux 4.7+内核,可以使用BPF来跟踪这个事件:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <linux/bpf.h> #include "bpf_helpers.h"
SEC("tracepoint/sched/sched_process_exec") int bpf_prog(struct trace_event_raw_sched_process_exec *ctx) { return 0; }
char _license[] SEC("license") = "GPL";
|
注意事项
- 需要确保内核配置了
CONFIG_TRACEPOINTS
和CONFIG_SCHED_TRACER
- 在生产环境中使用时,建议使用更高效的记录方式(如perf事件)而非printk
- 对于BPF方式,需要内核支持BPF并加载BPF程序
- 获取文件路径时要注意内核上下文限制
以上示例展示了不同方式注册sched_process_exec
tracepoint的方法,你可以根据具体需求选择适合的方式。
linux内核中trace_sched_process_exec中获取进程的启动参数的方法,要求不能有睡眠
在Linux内核的trace_sched_process_exec
跟踪点中获取进程启动参数需要谨慎处理,因为不能访问用户空间内存(可能引起睡眠/缺页中断),且参数可能分布在多个内存页中。以下是一个安全获取进程启动参数的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include <linux/trace_events.h> #include <linux/binfmts.h> #include <linux/string.h>
#define MAX_ARG_LEN 128
static void tp_sched_process_exec(void *ignore, struct task_struct *task, pid_t old_pid, struct linux_binprm *bprm) { char arg_buf[MAX_ARG_LEN]; int argc = bprm->argc; int i, offset = 0; printk(KERN_INFO "exec: pid=%d (%s) argc=%d\n", task->pid, bprm->filename, argc);
unsigned long __user *argv_user = (unsigned long __user *)bprm->p; for (i = 0; i < argc && offset < MAX_ARG_LEN - 1; i++) { char __user *argp; unsigned long arg_addr; if (copy_from_user(&arg_addr, &argv_user[i], sizeof(arg_addr))) break; argp = (char __user *)arg_addr; if (!argp) break; int len = strnlen_user(argp, MAX_ARG_LEN - offset - 1); if (len <= 0 || len > MAX_ARG_LEN - offset - 1) break; if (copy_from_user(arg_buf + offset, argp, len)) break; offset += len; arg_buf[offset++] = ' '; } if (offset > 0) { arg_buf[offset > 0 ? offset - 1 : 0] = '\0'; printk(KERN_INFO "argv: %s\n", arg_buf); } }
|
关键点说明:
安全访问用户空间:
- 使用
copy_from_user()
和strnlen_user()
避免直接访问用户空间
- 这些函数处理了可能发生的页面错误
参数长度限制:
- 使用
strnlen_user()
限制最大长度
MAX_ARG_LEN
防止缓冲区溢出
参数位置计算:
bprm->p
包含用户空间argv数组地址
bprm->argc
提供参数数量
内存安全:
替代方案(eBPF):
对于更新内核(4.17+),使用eBPF更安全高效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| SEC("tracepoint/sched/sched_process_exec") int trace_exec(struct trace_event_raw_sched_process_exec *ctx) { struct linux_binprm *bprm = (struct linux_binprm *)ctx->bprm; char args[256]; char *p = args; int i, max = sizeof(args) - 1;
for (i = 0; i < bprm->argc && max > 0; i++) { unsigned long arg_addr; bpf_probe_read_user(&arg_addr, sizeof(arg_addr), bprm->p + i * sizeof(unsigned long)); int len = bpf_probe_read_user_str(p, max, (char *)arg_addr); if (len <= 0) break; p += len - 1; *p++ = ' '; max -= len; } if (p > args) *(p-1) = 0; bpf_trace_printk("exec: %s %s\n", ctx->filename, args); return 0; }
|
注意事项:
内核版本差异:
- 不同内核版本中
linux_binprm
结构可能有变化
- 需要验证
bprm->p
和bprm->argc
的可用性
性能考虑:
- 限制参数总长度(建议256-512字节)
- 避免在频繁执行的路径中打印完整参数
安全边界:
生产环境建议:
- 优先使用eBPF方案
- 或通过perf events输出数据
- 避免在内核日志中记录敏感信息
使用这些方法可以在不睡眠的前提下安全地获取execve的参数,但需要注意参数截断的可能性(特别是长参数列表的情况)。
linux 5.10内核中trace_sched_process_exec中获取进程的启动参数的方法,要求不能有睡眠
在 Linux 5.10 内核中,从 trace_sched_process_exec
获取进程启动参数而不引起睡眠需要谨慎处理用户空间内存访问。以下是安全的方法:
安全获取启动参数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <linux/trace_events.h> #include <linux/binfmts.h> #include <linux/uaccess.h> #include <linux/string.h>
#define MAX_ARG_STRLEN 128 #define MAX_ARG_COUNT 32
static void tp_sched_process_exec(void *ignore, struct task_struct *task, pid_t old_pid, struct linux_binprm *bprm) { unsigned long argv_user[MAX_ARG_COUNT]; char arg_buf[MAX_ARG_STRLEN]; char args_flat[MAX_ARG_STRLEN * 2]; int i, offset = 0; int arg_count = min(bprm->argc, (int)MAX_ARG_COUNT); if (copy_from_kernel_nofault(argv_user, bprm->p, sizeof(unsigned long) * arg_count)) { trace_printk("Failed to copy argv pointers\n"); return; } args_flat[0] = '\0'; for (i = 0; i < arg_count && offset < sizeof(args_flat) - MAX_ARG_STRLEN; i++) { unsigned long arg_addr = argv_user[i]; int len; len = strnlen_user_nofault((const char __user *)arg_addr, MAX_ARG_STRLEN); if (len <= 0 || len > MAX_ARG_STRLEN) continue; if (copy_from_user_nofault(arg_buf, (const char __user *)arg_addr, len)) continue; arg_buf[len - 1] = '\0'; scnprintf(args_flat + offset, sizeof(args_flat) - offset, "%s%s", (i > 0) ? " " : "", arg_buf); offset += strlen(args_flat + offset); } trace_printk("exec: pid=%d cmd=%s args=%s\n", task->pid, bprm->filename, args_flat); }
|
关键安全措施
使用无睡眠的访问函数:
copy_from_kernel_nofault()
:用于访问内核空间数据
copy_from_user_nofault()
:用于访问用户空间数据
strnlen_user_nofault()
:安全获取用户空间字符串长度
缓冲区边界保护:
- 限制最大参数数量(
MAX_ARG_COUNT
)
- 限制单个参数长度(
MAX_ARG_STRLEN
)
- 使用
scnprintf()
防止缓冲区溢出
错误处理:
- 检查所有内存访问函数的返回值
- 跳过无效参数而非中止处理
内存安全:
Linux 5.10 特定实现细节
linux_binprm
结构变化:
1 2 3 4 5 6 7
| struct linux_binprm { int argc, envc; const char *filename; unsigned long p; };
|
无睡眠访问函数:
1 2 3 4 5
| long copy_from_user_nofault(void *dst, const void __user *src, size_t size);
long strnlen_user_nofault(const void __user *str, long len);
|
替代方案:使用 eBPF (推荐)
对于生产环境,推荐使用 eBPF 方案,更安全高效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| SEC("tracepoint/sched/sched_process_exec") int trace_exec(struct trace_event_raw_sched_process_exec *ctx) { struct linux_binprm *bprm = (struct linux_binprm *)ctx->bprm; unsigned long argv[MAX_ARGS]; char arg_buf[MAX_ARG_LEN]; char args_flat[MAX_TOTAL_LEN]; int i, offset = 0; bpf_probe_read_kernel(&argv, sizeof(argv), bprm->p); for (i = 0; i < min(bprm->argc, MAX_ARGS); i++) { if (offset >= MAX_TOTAL_LEN - MAX_ARG_LEN) break; int len = bpf_probe_read_user_str(arg_buf, sizeof(arg_buf), (const char *)argv[i]); if (len <= 0) continue; if (i > 0) { bpf_snprintf(args_flat + offset, MAX_TOTAL_LEN - offset, " "); offset++; } bpf_snprintf(args_flat + offset, MAX_TOTAL_LEN - offset, "%s", arg_buf); offset += len - 1; } bpf_trace_printk("exec: %s %s", ctx->filename, args_flat); return 0; }
|
注意事项
上下文约束:
trace_sched_process_exec
在 execve 系统调用上下文中运行
- 禁止任何可能睡眠的操作(如分配内存、等待锁等)
性能影响:
- 限制处理的参数数量和长度
- 避免复杂字符串处理
- 生产环境中建议使用 eBPF 的 perf 环形缓冲区
安全边界:
1 2 3
| #define MAX_ARG_STRLEN (PAGE_SIZE / 4) #define MAX_ARG_COUNT 64
|
内核版本适配:
- 不同内核版本中结构体可能变化
- 5.10 中
linux_binprm
包含 argc
和 p
成员
- 使用
#ifdef
处理版本差异
这种方法在 Linux 5.10 内核中安全地获取 execve 参数,同时遵守原子上下文的约束,避免了任何可能导致睡眠的操作。
Linux内核支持copy_from_user_nofault函数么
在 Linux 内核中,没有名为 copy_from_user_nofault
的标准函数。这是一个常见的误解,我需要澄清这一点并为您提供正确的替代方案。
正确的无睡眠用户空间访问函数
在 Linux 5.10 内核中,用于原子上下文的用户空间访问函数是:
copy_from_kernel_nofault()
用于从内核空间安全读取数据
copy_from_user_nofault()
不存在
正确的函数是:
1 2
| long probe_kernel_read(void *dst, const void *src, size_t size); long probe_user_read(void *dst, const void __user *src, size_t size);
|
在 Linux 5.10 中,这些函数被定义为:
1 2 3 4 5
| long probe_user_read(void *dst, const void __user *src, size_t size);
long probe_kernel_read(void *dst, const void *src, size_t size);
|
实际使用示例 (Linux 5.10)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <linux/uaccess.h> #include <linux/string.h>
#define MAX_ARG_LEN 128
static void tp_sched_process_exec(void *ignore, struct task_struct *task, pid_t old_pid, struct linux_binprm *bprm) { unsigned long argv_user[16]; char arg_buf[MAX_ARG_LEN]; char args_flat[512]; int i, offset = 0; int argc = min(bprm->argc, 16);
if (probe_kernel_read(argv_user, bprm->p, sizeof(unsigned long) * argc)) { trace_printk("Failed to copy argv pointers\n"); return; }
args_flat[0] = '\0';
for (i = 0; i < argc && offset < sizeof(args_flat) - MAX_ARG_LEN; i++) { unsigned long arg_addr = argv_user[i]; long len; len = probe_user_read(arg_buf, (const void __user *)arg_addr, MAX_ARG_LEN); if (len <= 0) continue; if (len > MAX_ARG_LEN) len = MAX_ARG_LEN; if (offset > 0 && offset < sizeof(args_flat) - 1) { args_flat[offset++] = ' '; } memcpy(args_flat + offset, arg_buf, len); offset += len; if (offset < sizeof(args_flat)) { args_flat[offset] = '\0'; } }
trace_printk("exec: pid=%d cmd=%s args=%s\n", task->pid, bprm->filename, args_flat); }
|
关键函数说明 (Linux 5.10)
函数 |
用途 |
是否可能睡眠 |
probe_user_read() |
从用户空间读取数据 |
不会睡眠 |
probe_kernel_read() |
从内核空间读取数据 |
不会睡眠 |
strncpy_from_user_nofault() |
安全复制用户空间字符串 |
不会睡眠 |
特别说明:strncpy_from_user_nofault
对于字符串操作,更优的选择是使用:
1
| long strncpy_from_user_nofault(char *dst, const void __user *src, long count);
|
使用示例:
1 2 3 4 5 6
| len = strncpy_from_user_nofault(arg_buf, (const void __user *)arg_addr, MAX_ARG_LEN); if (len < 0) { } else if (len == MAX_ARG_LEN) { arg_buf[MAX_ARG_LEN - 1] = '\0'; }
|
历史变化
Linux 4.11+:
1 2
| probe_user_read() probe_kernel_read()
|
Linux 5.8+:
引入了更一致的API:
1 2
| copy_from_user_nofault() copy_to_user_nofault()
|
但在5.10中仍建议使用probe_*
系列函数
Linux 5.14+:
最终统一为:
1 2
| copy_from_user_nofault() copy_to_user_nofault()
|
生产环境建议
对于实际使用,特别是需要高可靠性的场景:
优先使用 eBPF:
1 2 3 4 5 6 7 8 9 10
| SEC("tracepoint/sched/sched_process_exec") int trace_exec(struct trace_event_raw_sched_process_exec *ctx) { struct linux_binprm *bprm = (void *)ctx->bprm; char args[256]; bpf_probe_read_user_str(args, sizeof(args), (void *)bprm->p); bpf_printk("exec: %s %s", ctx->filename, args); return 0; }
|
使用内核内置功能:
1 2
| echo 'sched:sched_process_exec cmd=$cmd args=$args' > trace
|
如果必须编写内核模块:
- 使用
probe_user_read()
/strncpy_from_user_nofault()
- 严格限制缓冲区大小
- 处理所有可能的错误情况
- 避免在关键路径中打印完整参数
总之,在 Linux 5.10 中:
- 没有
copy_from_user_nofault()
函数
- 使用
probe_user_read()
或 strncpy_from_user_nofault()
替代
- eBPF 是更安全、更推荐的方法
使用strncpy_from_user_nofault获取用户态进程cmdline
在 Linux 内核中,使用 strncpy_from_user_nofault()
获取用户态进程的 cmdline
(即启动参数)是一种安全且高效的方法,尤其适用于 trace_sched_process_exec
这样的原子上下文。以下是完整的实现方案:
1. 核心实现代码(Linux 5.10+)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| #include <linux/trace_events.h> #include <linux/binfmts.h> #include <linux/uaccess.h> #include <linux/string.h>
#define MAX_ARG_LEN 256 #define MAX_ARG_COUNT 32 #define MAX_TOTAL_LEN 512
static void tp_sched_process_exec(void *ignore, struct task_struct *task, pid_t old_pid, struct linux_binprm *bprm) { unsigned long argv_user[MAX_ARG_COUNT]; char arg_buf[MAX_ARG_LEN]; char cmdline[MAX_TOTAL_LEN] = {0}; int i, offset = 0; int argc = min(bprm->argc, MAX_ARG_COUNT);
if (copy_from_kernel_nofault(argv_user, bprm->p, sizeof(unsigned long) * argc)) { trace_printk("Failed to copy argv pointers\n"); return; }
for (i = 0; i < argc && offset < MAX_TOTAL_LEN - 1; i++) { unsigned long arg_addr = argv_user[i]; long len;
len = strncpy_from_user_nofault(arg_buf, (const char __user *)arg_addr, MAX_ARG_LEN); if (len <= 0) continue;
arg_buf[min(len, MAX_ARG_LEN - 1)] = '\0';
if (i > 0 && offset < MAX_TOTAL_LEN - 1) { cmdline[offset++] = ' '; }
strscpy(cmdline + offset, arg_buf, MAX_TOTAL_LEN - offset); offset += strlen(arg_buf); }
trace_printk("exec: pid=%d cmd=%s args=%s\n", task->pid, bprm->filename, cmdline); }
|
2. 关键点解析
(1) 安全访问用户空间
(2) 边界保护
(3) 原子上下文约束
- 禁止睡眠操作
所有函数(strncpy_from_user_nofault
、copy_from_kernel_nofault
)均保证在原子上下文中安全执行。
3. 替代方案:eBPF(推荐用于生产环境)
若内核支持 eBPF,以下方案更安全高效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| SEC("tracepoint/sched/sched_process_exec") int trace_exec(struct trace_event_raw_sched_process_exec *ctx) { struct linux_binprm *bprm = (struct linux_binprm *)ctx->bprm; unsigned long argv[MAX_ARG_COUNT]; char cmdline[MAX_TOTAL_LEN] = {0}; int i, offset = 0;
bpf_probe_read_kernel(&argv, sizeof(argv), bprm->p);
for (i = 0; i < min(bprm->argc, MAX_ARG_COUNT); i++) { char arg[MAX_ARG_LEN]; long len;
len = bpf_probe_read_user_str(arg, sizeof(arg), (void *)argv[i]); if (len <= 0) continue;
if (i > 0) { bpf_snprintf(cmdline + offset, MAX_TOTAL_LEN - offset, " "); offset++; } bpf_snprintf(cmdline + offset, MAX_TOTAL_LEN - offset, "%s", arg); offset += len - 1; }
bpf_trace_printk("exec: %s %s", ctx->filename, cmdline); return 0; }
|
4. 注意事项
内核版本差异
strncpy_from_user_nofault
在 Linux 5.10+ 中可用,旧版本需用 probe_user_read()
。
linux_binprm
结构体可能随版本变化,需验证成员偏移量。
性能优化
- 生产环境中避免打印完整
cmdline
,可只记录关键信息(如哈希值)。
- 对高频事件(如容器启动),建议通过 eBPF 的
perf_submit
异步处理数据。
安全风险
- 用户空间参数可能包含恶意构造的超长字符串,需严格限制长度。
- 敏感信息(如密码)可能出现在参数中,记录时需脱敏。
5. 验证方法
加载模块后,通过 dmesg
或 trace_pipe
查看输出:
1 2 3 4 5
| dmesg -wH
cat /sys/kernel/debug/tracing/trace_pipe
|
输出示例:
1
| exec: pid=1234 cmd=/usr/bin/bash args=-c echo hello
|
以上方案在保证安全性的同时,完整获取了进程的启动参数,适用于调试、监控或安全审计场景。