当前位置: 首页 > news >正文

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_SIZE
    • TASK_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,并递增 esi
  • stosb: 将 al 存储到 %edi (dst),并递增 edi
  • testb %%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) 移动到 res
  • jmp 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_struct
  • audit_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;
}
  • 递增计数器: 增加已记录文件名的数量
  • 为下一次记录做准备: 更新索引位置
http://www.dtcms.com/a/533636.html

相关文章:

  • 台州网站建设企业精品网站建设教程
  • 触屏版网站模板怎么把网站变成免费的
  • 手机网站建设事项京东商城网上购物商城
  • 卖鞋的网站建设思路河北建设部网站
  • 广州网站建设求职简历wordpress加联系方式
  • 迟到的加入CSDN周年纪念
  • 惠州网站建设多少钱闸北网站建设
  • FFmpeg 基本数据结构 AVInputFormat 分析
  • 在哪个网站做视频好赚钱做企业公司网站
  • 基于docker打包code server镜像的范例(2025/10/26更新)
  • 邻接矩阵的 k 次幂意味着什么?从图论到路径计数的直观解释
  • 海兴县做网站价格wordpress后台超慢
  • 实战案例:某电商网站反爬策略分析与绕过过程记录
  • 网站开发有哪些内容中国建设教育协会是个什么网站
  • 昆明市建设局网站台州建设监理协会网站
  • 《Linux篇》命令行参数与环境变量
  • vue做的网站wordpress存档
  • 建设银行网站无法登陆网站服务器怎么选
  • 淘宝网商城商丘seo公司甄选24火星
  • 云服务器上安装JDK
  • Python字符串操作:如何判断子串是否存在
  • 打工人日报#20251026
  • 松原网站建设公司电话自己建设自己的网站
  • 制作应用的网站网上商城 网站
  • 一文速通k8s基础概念原理Kubernetes
  • 精品成品源码网站武功网站建设
  • 做网站维护学什么编程语言百度网站推广怎么做
  • 手机网站html模板建筑模板厂投资多少钱
  • 英文网站一般用什么字体酒店网站建设背景分析
  • 可重入函数 与 不可重入函数