Linux中内核从用户空间获取路径名getname函数的实现
getname函数概览
调用流程
getname → do_getname → strncpy_from_user → __do_strncpy_from_user↓audit_getname (审计记录)
核心安全机制
多层地址验证
access_ok()/__range_ok(): 检查用户地址有效性- 段描述符检查: 防止用户程序传递内核地址
- 边界检查: 防止读取超出用户空间
异常处理
__ex_table异常表: 优雅处理页面故障.fixup段: 提供错误恢复代码
设计哲学体现
深度防御
- 每层都有独立的安全检查
- 不信任任何用户输入
- 假设可能发生各种错误情况
可维护性
- 清晰的错误码传递
- 丰富的调试支持
从用户空间安全地复制文件名/路径名到内核空间getname
#define __getname() kmem_cache_alloc(names_cachep, SLAB_KERNEL)
static inline long IS_ERR(const void *ptr)
{return unlikely((unsigned long)ptr > (unsigned long)-1000L);
}
static inline void *ERR_PTR(long error)
{return (void *) error;
}
char * getname(const char __user * filename)
{char *tmp, *result;result = ERR_PTR(-ENOMEM);tmp = __getname();if (tmp) {int retval = do_getname(filename, tmp);result = tmp;if (retval < 0) {__putname(tmp);result = ERR_PTR(retval);}}if (unlikely(current->audit_context) && !IS_ERR(result) && result)audit_getname(result);return result;
}
函数功能概述
getname 函数用于从用户空间安全地复制文件名/路径名到内核空间,是文件系统操作中常用的辅助函数
代码详细分析
宏定义和辅助函数
#define __getname() kmem_cache_alloc(names_cachep, SLAB_KERNEL)
__getname(): 从专用的名称缓存 (names_cachep) 中分配内存kmem_cache_alloc: 内核内存池分配器,比普通kmalloc更高效SLAB_KERNEL: 分配标志,表示用于内核内部使用
static inline long IS_ERR(const void *ptr)
{return unlikely((unsigned long)ptr > (unsigned long)-1000L);
}
IS_ERR(): 检查指针是否包含错误码- 原理: 内核空间的高地址区域用于存储错误码(通常为负值)
unlikely: 告诉编译器这个条件很少成立,帮助优化分支预测-1000L: 错误码的边界值
static inline void *ERR_PTR(long error)
{return (void *) error;
}
ERR_PTR: 将错误码转换为指针形式,便于在指针返回的函数中使用
函数声明和变量定义
char * getname(const char __user * filename)
{char *tmp, *result;result = ERR_PTR(-ENOMEM);
- 参数:
filename- 来自用户空间的路径名字符串 - 变量:
tmp: 临时指针,指向分配的内核缓冲区result: 最终返回的结果指针
- 初始化: 先将结果预设为"内存不足"错误 (
-ENOMEM)
内存分配和基本检查
tmp = __getname();if (tmp) {int retval = do_getname(filename, tmp);
__getname(): 从名称缓存分配一个路径名缓冲区if (tmp): 检查内存是否分配成功do_getname(filename, tmp): 实际执行从用户空间复制的函数,将用户空间的文件名复制到内核缓冲区tmp中
复制结果处理
result = tmp;if (retval < 0) {__putname(tmp);result = ERR_PTR(retval);}}
- 成功路径: 将结果设置为分配的内核缓冲区
tmp - 失败处理:
- 如果
do_getname返回错误(负值) __putname(tmp): 释放之前分配的名称缓存result = ERR_PTR(retval): 将错误码转换为错误指针
- 如果
审计跟踪处理
if (unlikely(current->audit_context) && !IS_ERR(result) && result)audit_getname(result);
- 条件检查:
unlikely(current->audit_context): 当前进程有审计上下文!IS_ERR(result): 操作成功(不是错误指针)result: 结果指针有效
audit_getname(result): 如果启用了审计子系统,记录文件名用于审计追踪
返回结果
return result;
}
- 返回最终的结果指针,可能是:
- 成功:指向内核空间文件名的指针
- 失败:包含错误码的错误指针
复制文件路径名到内核空间缓冲区do_getname
#define segment_eq(a,b) ((a).seg == (b).seg)
static inline int do_getname(const char __user *filename, char *page)
{int retval;unsigned long len = PATH_MAX;if ((unsigned long) filename >= TASK_SIZE) {if (!segment_eq(get_fs(), KERNEL_DS))return -EFAULT;} else if (TASK_SIZE - (unsigned long) filename < PATH_MAX)len = TASK_SIZE - (unsigned long) filename;retval = strncpy_from_user((char *)page, filename, len);if (retval > 0) {if (retval < len)return 0;return -ENAMETOOLONG;} else if (!retval)retval = -ENOENT;return retval;
}
函数功能概述
do_getname 函数负责从用户空间安全地复制文件路径名到内核空间缓冲区,并进行必要的边界检查和错误处理
代码详细分析
宏定义和函数声明
#define segment_eq(a,b) ((a).seg == (b).seg)
segment_eq: 比较两个内存段描述符是否相等- 在内核中用于检查当前的内存空间设置
static inline int do_getname(const char __user *filename, char *page)
{int retval;unsigned long len = PATH_MAX;
- 参数:
filename: 用户空间的路径名字符串指针page: 内核空间的目标缓冲区
- 局部变量:
retval: 存储操作结果len: 要复制的最大长度,初始化为PATH_MAX(通常为4096)
地址空间检查
if ((unsigned long) filename >= TASK_SIZE) {if (!segment_eq(get_fs(), KERNEL_DS))return -EFAULT;}
- 第一层检查: 如果
filename地址 >=TASK_SIZETASK_SIZE: 用户空间的最大地址边界,32位系统是0xC0000000- 这种情况表示传入的是内核空间地址
- 权限验证:
get_fs()即#define get_fs() (current_thread_info()->addr_limit)- 获取当前线程的地址限制
addr_limit: 线程信息中的一个字段,定义了该线程可以访问的地址空间上限- 在用户空间线程中,这个值通常是
TASK_SIZE(0xC0000000) - 在内核线程中,这个值被设置为
KERNEL_DS(0xFFFFFFFF)
KERNEL_DS: 内核线程地址限制- 防止用户程序故意传递内核地址或者用户程序bug导致指针错误
- 如果是内核线程调用,允许继续执行
边界长度计算
else if (TASK_SIZE - (unsigned long) filename < PATH_MAX)len = TASK_SIZE - (unsigned long) filename;
- 第二层检查: 计算从当前地址到用户空间末尾的距离
- 防止越界: 如果剩余空间小于
PATH_MAX,调整复制长度为剩余空间 - 安全性: 确保不会读取超出用户空间边界的内存
执行字符串复制
retval = strncpy_from_user((char *)page, filename, len);
strncpy_from_user: 内核提供的安全复制函数- 功能: 从用户空间复制字符串到内核空间,会在遇到空终止符时停止
- 参数:
page: 目标内核缓冲区filename: 源用户空间指针len: 最大复制长度
- 返回值:
- 成功: 复制的字符数(不包括终止符)
- 错误: 负的错误码
复制结果处理
if (retval > 0) {if (retval < len)return 0;return -ENAMETOOLONG;}
- 成功复制 (
retval > 0):- 正常情况 (
retval < len): 字符串完整复制,返回0表示成功- 使用小于而不是小于等于是因为
retval是不包含终止符的
- 使用小于而不是小于等于是因为
- 路径过长 (
retval == len): 缓冲区已满但未遇到终止符,返回-ENAMETOOLONG
- 正常情况 (
错误情况处理
else if (!retval)retval = -ENOENT;return retval;
- 空字符串 (
retval == 0): 转换为-ENOENT(文件不存在) - 其他错误: 直接返回
strncpy_from_user的错误码
安全机制详解
地址验证机制
// 防止内核空间地址在用户上下文使用
if ((unsigned long) filename >= TASK_SIZE) {if (!segment_eq(get_fs(), KERNEL_DS))return -EFAULT;
}
边界保护机制
// 防止读取超出用户空间边界
else if (TASK_SIZE - (unsigned long) filename < PATH_MAX)len = TASK_SIZE - (unsigned long) filename;
复制结果验证
retval = strncpy_from_user((char *)page, filename, len);
if (retval > 0) {if (retval < len)return 0; // 成功: 找到终止符return -ENAMETOOLONG; // 失败: 路径过长
}
从用户空间安全地复制字符串到内核空间strncpy_from_user
long
strncpy_from_user(char *dst, const char __user *src, long count)
{long res = -EFAULT;if (access_ok(VERIFY_READ, src, 1))__do_strncpy_from_user(dst, src, count, res);return res;
}
#define __do_strncpy_from_user(dst,src,count,res) \
do { \long __d0, __d1, __d2; \might_sleep(); \__asm__ __volatile__( \" testq %1,%1\n" \" jz 2f\n" \"0: lodsb\n" \" stosb\n" \" testb %%al,%%al\n" \" jz 1f\n" \" decq %1\n" \" jnz 0b\n" \"1: subq %1,%0\n" \"2:\n" \".section .fixup,\"ax\"\n" \"3: movq %5,%0\n" \" jmp 2b\n" \".previous\n" \".section __ex_table,\"a\"\n" \" .align 8\n" \" .quad 0b,3b\n" \".previous" \: "=r"(res), "=c"(count), "=&a" (__d0), "=&S" (__d1), \"=&D" (__d2) \: "i"(-EFAULT), "0"(count), "1"(count), "3"(src), "4"(dst) \: "memory"); \
} while (0)
函数功能概述
strncpy_from_user 用于从用户空间安全地复制字符串到内核空间,是内核中关键的安全复制函数
外层函数分析
long strncpy_from_user(char *dst, const char __user *src, long count)
{long res = -EFAULT;if (access_ok(VERIFY_READ, src, 1))__do_strncpy_from_user(dst, src, count, res);return res;
}
- 参数:
dst: 目标内核缓冲区src: 源用户空间字符串count: 最大复制字节数
- 初始化:
res = -EFAULT,预设为"错误的地址"错误 - 安全检查:
access_ok(VERIFY_READ, src, 1)验证用户地址是否可读- 检查地址是否溢出
- 检查地址是否超过用户空间地址限制
- 核心操作: 如果地址有效,调用内联汇编实现的实际复制
内联汇编宏详细解析
宏定义和准备
#define __do_strncpy_from_user(dst,src,count,res) \
do { \long __d0, __d1, __d2; \might_sleep(); \
do {...} while(0): 创建代码块,确保宏使用安全- 寄存器变量:
__d0, __d1, __d2用于内联汇编的临时寄存器 might_sleep(): 提示该操作可能引起调度(由于页面故障),确保不是原子上下文,否则在调试环境报错
汇编开始和初始检查
__asm__ __volatile__( \" testq %1,%1\n" \" jz 2f\n" \
__volatile__: 告诉编译器不要优化这段汇编testq %1,%1: 测试 count 参数(%1)jz 2f: 如果 count == 0,跳转到标签 2(直接返回)
主复制循环
"0: lodsb\n" \" stosb\n" \" testb %%al,%%al\n" \" jz 1f\n" \" decq %1\n" \" jnz 0b\n" \
0: lodsb: 从%esi (src)加载字节到al,并递增esistosb: 将al存储到%edi (dst),并递增editestb %%al,%%al: 测试刚复制的字节是否为0(字符串结束)jz 1f: 如果是0,跳转到标签1(计算结果)decq %1: 递减 count 计数器jnz 0b: 如果 count != 0,继续循环
结果计算
"1: subq %1,%0\n" \"2:\n" \
1: subq %1,%0: 计算 res = 初始count - 剩余count(%0 = %0 - %1)- 结果就是实际复制的字符数(不包括终止符)
2:: 结束标签
错误处理段
".section .fixup,\"ax\"\n" \"3: movq %5,%0\n" \" jmp 2b\n" \".previous\n" \
.section .fixup,"ax": 定义修复段,用于处理页面故障3: movq %5,%0: 发生错误时,将%5 (-EFAULT)移动到 resjmp 2b: 跳转到结束标签.previous: 恢复之前的代码段,在编译时有用,告诉编译器段的内存布局
异常表
".section __ex_table,\"a\"\n" \" .align 8\n" \" .quad 0b,3b\n" \".previous" \
- 关键的安全机制:
__ex_table: 异常处理表.quad 0b,3b: 建立映射:如果标签0(主循环)发生页面故障,跳转到标签3(错误处理)- 因为内核地址
dst有可能还没有建立物理地址映射
- 因为内核地址
: "=r"(res), "=c"(count), "=&a" (__d0), "=&S" (__d1), \"=&D" (__d2) \: "i"(-EFAULT), "0"(count), "1"(count), "3"(src), "4"(dst) \: "memory"); \
} while (0)
输出操作数:
"=r"(res): res 使用任一寄存器,结果将存储到这里"=c"(count): count 使用ecx寄存器"=&a" (__d0): __d0 使用eax寄存器(lodsb/stosb使用)"=&S" (__d1): __d1 使用esi寄存器(源指针)"=&D" (__d2): __d2 使用edi寄存器(目标指针)
输入操作数:
"i"(-EFAULT): 立即数 -EFAULT"0"(count): 与输出操作数0(res)使用相同位置"1"(count): 与输出操作数1(count)使用相同位置"3"(src): 与输出操作数3(esi)绑定"4"(dst): 与输出操作数4(edi)绑定
破坏描述:
"memory": 告诉编译器内存被修改,防止优化
记录系统调用中使用的文件名audit_getname
void audit_getname(const char *name)
{struct audit_context *context = current->audit_context;BUG_ON(!context);if (!context->in_syscall) {
#if AUDIT_DEBUG == 2printk(KERN_ERR "%s:%d(:%d): ignoring getname(%p)\n",__FILE__, __LINE__, context->serial, name);dump_stack();
#endifreturn;}BUG_ON(context->name_count >= AUDIT_NAMES);context->names[context->name_count].name = name;context->names[context->name_count].ino = (unsigned long)-1;context->names[context->name_count].rdev = -1;++context->name_count;
}
函数功能概述
audit_getname 是 Linux 内核审计子系统的一部分,用于记录系统调用中使用的文件名,以便后续审计追踪
代码详细分析
函数声明和上下文获取
void audit_getname(const char *name)
{struct audit_context *context = current->audit_context;
- 参数:
name- 要记录的文件名字符串(已经在内核空间) current: 宏,指向当前正在运行的进程的task_structaudit_context: 当前进程的审计上下文结构体指针- 获取当前进程的审计上下文,用于记录审计信息
健壮性检查
BUG_ON(!context);
BUG_ON(!context): 内核断言,如果context为 NULL 则触发内核错误- 作用: 确保审计上下文存在,这是审计记录的基本前提
- 后果: 如果断言失败,内核会 panic 并输出调试信息
系统调用上下文验证
if (!context->in_syscall) {
#if AUDIT_DEBUG == 2printk(KERN_ERR "%s:%d(:%d): ignoring getname(%p)\n",__FILE__, __LINE__, context->serial, name);dump_stack();
#endifreturn;}
- 条件检查:
!context->in_syscall- 检查是否在系统调用上下文中 - 调试输出 (当
AUDIT_DEBUG == 2时):printk: 输出错误信息,包括文件名、行号、审计序列号和文件名指针dump_stack(): 打印内核调用栈,用于调试
- 处理: 如果不在系统调用中,直接返回(不记录文件名)
数组边界检查
BUG_ON(context->name_count >= AUDIT_NAMES);
AUDIT_NAMES: 常量,定义每个系统调用最多记录的文件名数量- 检查: 确保当前文件名数量没有超过最大限制,因为记录名字的数组就
AUDIT_NAMES这么大 - 目的: 防止数组越界访问
记录文件名信息
context->names[context->name_count].name = name;context->names[context->name_count].ino = (unsigned long)-1;context->names[context->name_count].rdev = -1;
- 设置文件名: 将文件名指针存储到审计上下文中
- 初始化
inode号: 设置为(unsigned long)-1(无效值),后续会填充实际值 - 初始化设备号: 设置为
-1(无效值),后续会填充实际值 - 注意: 此时只记录文件名地址,不复制字符串内容
更新计数器
++context->name_count;
}
- 递增计数器: 增加已记录文件名的数量
- 为下一次记录做准备: 更新索引位置
