X86 SMAP(Supervisor Mode Access Prevention)机制引入的内核态访问用户态地址空间的问题分析

本文主要是介绍X86 SMAP(Supervisor Mode Access Prevention)机制引入的内核态访问用户态地址空间的问题分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在Linux系统中,当涉及到用户态和内核态数据拷贝的时候,如果不考虑建立kernel space和user space的共享映射实现的零拷贝情况,一般是调用copy_from_user/copy_to_user/put_user/get_user几组宏来实现的。在早些时候,对于用户态指针非法(没有VMA)或者缺页(有VMA但是MMU没有映射)两种情况,在函数实现中使用修复表进行处理,前一种情况下会返回错误码,第二种情况则通过page fault流程建立物理PFN映射,之后在进入修复表流程完成剩下的操作。而对于合法的用户态指针,则直接在内核中访问即可。所以,从内核中可以直接访问当前进程的用户空间,所使用的虚拟地址也与当前进程处于用户空间时的地址完全相同。反之则不允许。

但是这个结论似乎在运行最新linux的 x86 平台上遇到了反例,即便在内核中访问合法的用户态地址,也会被禁止,做个实验:

在设备驱动中直接读取用户态传递下来的buf指针的内容:

运行用户态用例,发现测试进程被KILL,内核报告permission违例。

仔细分析出错LOG,发现出错原因是#PF: error_code(0x0001) - permissions violation,许可违例。分析现场,发现出错地址0x7ffc922277b0和内核report的fault地址是一致的,并且,此虚拟地址对应的四级页表都有映射(PGD 23144d067 P4D 23144d067 PUD 2315d8067 PMD 23b0e9067 PTE, P4D和PGD重合)。也就是说,被访问的虚拟地址既不是非法地址,也没有缺页,是一个合法的用户态地址,按照本文开头的分析结论,此地址应该能够被内核安全访问才对,但是却报错了。

原因分析:

出现这个问题的原因和硬件架构和内核版本都有关系,最根本的原因是CPU硬引入了一个新的功能引起的,在最新的X86处理器的CR4寄存器中,引入了SMEP和SMAP 控制BIT,用来配置内核对用户态地址空间的访问权限。SMAP(Supervisor Mode Access Prevention)是Intel从Haswell微架构开始引入的一种新特征,它在CR4寄存器上引入一个新标志位SMAP,如果这个标志为1,内核访问用户进程的地址空间时就会触发一个页错误,目的是为了防止内核因为自身错误意外访问用户空间,这样就可以避免一些内核漏洞所导致的安全问题.但是由于内核在有些时候仍然需要访问用户空间,因此intel提供了两条指令STAC和CLAC用于临时打开/关闭这个功能,反复使用STAC和CLAC会带来一些轻微的性能损失,但考虑到增加的安全性,还是建议开启.

SMEP:位于Cr4的第20位,作用是让处于内核权限的CPU无法执行用户代码。
SMAP:位于Cr4的第21位,作用是让处于内核权限的CPU无法读写用户代码。

可以通过如下命令查看CPU是否支持smap功能,如下图所示,我的8核处理器每个核心都支持SMAP。

$ sudo cpuid|grep -i smap

另一台12核AMD处理器SMAP支持情况

虚拟机系统不支持SMAP,所以在虚拟机中进行内核直接访问用户态指针的测试是成功的。

内核中有配置选项CONFIG_X86_SMAP用来启用或者关闭SMAP功能,默认情况下是打开的.

那么为什么通过copy_from_user/put_user宏可以安全访问用户态指针呢?

以get_user为例,其它宏定义实现类似,在get_user的核心实现__get_user_1中,在进行真正的用户态指针访问前后,程序调用了ASM_STAC/ASM_CLAC去打开/关闭内核访问用户地址空间权限的功能:

并且在标号1处的修正表实现中也调用了CLAC指令对SMAP功能进行了控制,所以get_user才能安全地访问用户地址空间。

内核中clac/stac两条指令是以byte code形式定义的,我们可以DUMP kernel  __get_user_1函数的实现,看其访存调用是否被stac/clac指令包围:

确信__get_user_1指令中的访存被stac/clac指令包围,这样就不需要触发permisson violation异常执行exception_table_entry中的修复代码了。

但观察运行内核对应的vmlinux文件反编译文件中的__get_user_1函数实现,发现其对应的stac/clac指令区域全部为NOP,推测可能是运行阶段程序会对NOP区域进行指令修改,将NOP替换为stac/clac指令。但是具体什么时候在哪里做的,暂不清楚。

直接调用CPU指令的方式不太优雅,内核提供了两个函数,分别是define user_access_begin和user_access_end,用来供驱动开发者设置安全访问用户空间的代码区域,本质是对两条指令的封装。

用这两个调用保护访问用户内存的区域,重新测试:

发现程序正常执行,没有被KILL,说明两个接口起到了作用,内核态顺利访问了用户态的地址空间。

作为对比,尝试关闭SMAP,重新编译内核,确认是否可以在内核中直接访问用户态地址:

测试发现,在关闭SMAP机制后,即便不调用stac/clac指令,在内核模块中也可以直接访问用户态的地址了:

另外稍微留意__get_user_1的实现会发现,它仅支持源地址为用户空间的访问申请,如果传入的源地址位于内核空间,将会直接跳转到bad_get_user标志出返回错误。这就是专业的API,内核态访问内核指针当然没问题,但是因为是get_user场景,那就应该有所为,有所不为,非不能也,实不为也。

流程分析

当关闭SMAP检查,执行标号1处的访存指令发生异常,触发do_page_fault后, 系统会通过调用链do_page_fault->....->no_context->fixup_exception->search_exception_tables->handler...

执行ex_table中fixup字段指向的修复指令,1b对应的修复指令在.Lbad_get_user_clac,执行到这里首先会打开在访存前关闭的SMAP,之后返回错误码给应用。

为什么做的这么复杂呢?毕竟用户态的缺页处理对开发人员是透明的,系统能够保证即使缺页发生也能透明的向指定虚拟地址进行正确操作,为什么到了内核就不行了呢?必须要通过修复表显示修复。

个人的理解,如果仅仅是针对缺页的情况进行处理,内核page fault流程也可以做到提交物理页面后返回内核异常发生点继续处理。但是有一种情况比较特殊,内核无法参考用户态进程的处理方式,这种情况就是copy_to_user等宏接受到的地址是非法地址(没有在VMA区间),如果用户态发生这种情况,系统内核可以简单的发送一条KILL信号杀掉出问题的进程,但是copy_to_user是在内核态调用的,发生在内核态的非法地址异常的进程不能简单的通过发送KILL信号杀掉,因为系统不清楚在执行到异常点之前,内核代理的执行流是否有获取内核资源,比如锁,内存,信号等等,这个信息只有执行流本身清楚,所以,最好的办法是返回一个错误码,让执行流清理现场资源,并且带着错误码返回给用户态,让用户态决定是退出还是尝试继续系统调用。返回错误码的方式,就是将bad_get_user加入到 fixup修复表中,如下图:

这也是为什么当在内核中使用get_user操作一个用户态传入的一个非法地址的时候,进程会返回错误码,而不会报告任何其它异常的缘故吧。

毕竟,出了错用户态进程有内核兜底,自己可以做一个甩手掌柜,而内核态凡是必须亲历亲为,不能有一点闪失。

修复表

修复表一般用来恢复合法的用户态地址访问过程中,出现的缺页问题,在do page fault中提交PFN后,强制修改regs->ip返回地址为修复表地址,这样从缺页异常返回后就可以执行修复表中的指令完成恢复。

修复表位于内核ELF文件中的一个段,运行时地址在__start___ex_table和__stop___ex_table符号定义的区间内。

也可以将其DUMP出来进行分析,观察下图,注意4590行的输出,以下面这行为例:

extable_test line 4590, insn load_ucode_bsp+0xd7/0x1f0, fixup load_ucode_bsp+0xd9/0x1f0.

表示的是当load_ucode_bsp+0xd7/0x1f0 发生异常时,page fault异常要返回到 load_ucode_bsp+0xd9/0x1f0地址进行修复。

以__get_user_1修复表项为例:

修复表内容是:

extable_test line 4632, insn __get_user_1+0xd/0x20, fixup __get_user_nocheck_8+0x20/0x40.

它的exception instruction 指令在__get_user_1+0xd/0x20,修复指令位于 __get_user_nocheck_8+0x20/0x40,内核源码,反编译指令以及上述打印的__get_user_1修复表输出是一致的,见下图:

总结:

所以,内核直接访问用户态指针导致报告page fault错误的原因是直接访问用户态内存触发了SMAP保护,内核提供了配置和API接口关闭这种保护,而本文开头提到的几个宏定义能够安全将访问用户态内存的原因,也是由于在访问器件,关闭了SMAP保护。

从普遍意义的角度来讲,内核态访问用户态地址空间是没有任何问题的,只是需要注意不同的架构下实现上会有微小差别,至少目前,没有看到其它架构有类似SMAP机制的实现。

参考文章

https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf

Supervisor Memory Protection - OSDev Wiki


结束

这篇关于X86 SMAP(Supervisor Mode Access Prevention)机制引入的内核态访问用户态地址空间的问题分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/307207

相关文章

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

MySQL中的锁机制详解之全局锁,表级锁,行级锁

《MySQL中的锁机制详解之全局锁,表级锁,行级锁》MySQL锁机制通过全局、表级、行级锁控制并发,保障数据一致性与隔离性,全局锁适用于全库备份,表级锁适合读多写少场景,行级锁(InnoDB)实现高并... 目录一、锁机制基础:从并发问题到锁分类1.1 并发访问的三大问题1.2 锁的核心作用1.3 锁粒度分

Redis出现中文乱码的问题及解决

《Redis出现中文乱码的问题及解决》:本文主要介绍Redis出现中文乱码的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 问题的产生2China编程. 问题的解决redihttp://www.chinasem.cns数据进制问题的解决中文乱码问题解决总结

MySQL中的InnoDB单表访问过程

《MySQL中的InnoDB单表访问过程》:本文主要介绍MySQL中的InnoDB单表访问过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、访问类型【1】const【2】ref【3】ref_or_null【4】range【5】index【6】

MySQL之InnoDB存储页的独立表空间解读

《MySQL之InnoDB存储页的独立表空间解读》:本文主要介绍MySQL之InnoDB存储页的独立表空间,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、独立表空间【1】表空间大小【2】区【3】组【4】段【5】区的类型【6】XDES Entry区结构【

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

全面解析MySQL索引长度限制问题与解决方案

《全面解析MySQL索引长度限制问题与解决方案》MySQL对索引长度设限是为了保持高效的数据检索性能,这个限制不是MySQL的缺陷,而是数据库设计中的权衡结果,下面我们就来看看如何解决这一问题吧... 目录引言:为什么会有索引键长度问题?一、问题根源深度解析mysql索引长度限制原理实际场景示例二、五大解决

Springboot如何正确使用AOP问题

《Springboot如何正确使用AOP问题》:本文主要介绍Springboot如何正确使用AOP问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录​一、AOP概念二、切点表达式​execution表达式案例三、AOP通知四、springboot中使用AOP导出