您当前的位置:首页 > 电脑百科 > 软件技术 > 操作系统 > linux

LSM Oops 内存错误根因分析与解决

时间:2020-09-03 15:54:26  来源:  作者:

LSM Oops 内存错误根因分析与解决

Oops 是 linux 内核中最常见的一种异常出错情况,本文通过分析一个具体的 Oops 例子,介绍了如何分析并解决该类问题。

1. LSM 内核模块出现 Oops

insmod 一个 LSM 的 ko 模块,内核打印如下日志:

[  415.746844] BUG: unable to handle kernel paging request at ffffffffaa6f0210
[  415.746846] PGD 3fc0e067 P4D 3fc0e067 PUD 3fc0f063 PMD 34367063 PTE 800000003faf0061
[  415.746849] Oops: 0003 [#1] SMP PTI
[  415.746851] CPU: 0 PID: 8366 Comm: insmod Tainted: G           OE     4.19.82-wwh #1
[  415.746852] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/29/2019
[  415.746856] RIP: 0010:init_lsm_hooks+0x1cd/0x1f0 [xxx_security]
[  415.746857] Code: 00 00 48 8b 41 10 48 89 71 20 48 8b 10 48 85 d2 75 05 eb 25 48 89 c2 48 8b 02 48 85 c0 75 f5 48 c7 01 00 00 00 00 48 89 51 08 <48> 89 0a 48 83 c1 28 48 39 f9 75 cc 31 c0 c3 48 c7 01 00 00 00 00
[  415.746858] RSP: 0018:ffffa763c332fcb0 EFLAGS: 00010246
[  415.746859] RAX: 0000000000000000 RBX: 0000000000000000 RCX: ffffffffc0ae2020
[  415.746859] RDX: ffffffffaa6f0210 RSI: ffffffffc0adfaa9 RDI: ffffffffc0ae2458
[  415.746860] RBP: ffffffffc0ada000 R08: 000000000000006d R09: ffffffffaa5c11f3
[  415.746860] R10: ffffffffaa63c310 R11: ffffffffaa63c6a0 R12: ffffffffc0ae2fc0
[  415.746861] R13: ffffffffc0ae2e58 R14: ffffa763c332fe98 R15: ffffffffc0ae2e40
[  415.746862] FS:  00007fdf33491200(0000) GS:ffff9ac97bc00000(0000) knlGS:0000000000000000
[  415.746863] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  415.746863] CR2: ffffffffaa6f0210 CR3: 0000000077222004 CR4: 00000000003606f0
[  415.746880] Call Trace:
[  415.746883]  init_module+0x34/0xc0 [xxx_security]
[  415.746886]  do_one_initcall+0x46/0x1c3
[  415.746889]  ? _cond_resched+0x15/0x30
[  415.746890]  ? kmem_cache_alloc_trace+0x155/0x1d0
[  415.746892]  do_init_module+0x5a/0x210
[  415.746894]  load_module+0x215b/0x2390
[  415.746897]  ? __do_sys_finit_module+0xa8/0x110
[  415.746898]  __do_sys_finit_module+0xa8/0x110
[  415.746900]  do_syscall_64+0x55/0xf0
[  415.746901]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[  415.746910] RIP: 0033:0x7fdf335abf59

2. Oops 访存出错地址分析

首先看第一行:

 BUG: unable to handle kernel paging request at ffffffffaa6f0210

Unable to handle kernel paging request at virtual address 是内存访问异常的错误,原因通常有以下三种:

  1. virtual address 为 0x00000000 时,说明使用了空指针;
  2. virtual address 没有越出内核地址空间范围,说明指针指向的内存受到某种限制;
  3. 除此以外就是指针越出内核地址空间范围;

以上日志中的出错地址 ffffffffaa6f0210 在内核地址空间范围,可以判断为试图篡改受限制内存导致报错;例如给一个声明为 const 的变量赋值就会出现这种错误。

3. 引起 Oops 的内核代码位置初步分析

接下来看看日志中的 RIP 信息:

RIP: 0010:init_lsm_hooks+0x1cd/0x1f0 [xxx_security]

通常,我们在这种情况下从 Oops 收集的最有用的信息是 EIP 和错误的调用地址。对于 64bit 用户来说,你可能需要查看 RIP, EIP/RIP 通常标识了问题发生的现场。在这个例子中我们可以看到 Oops 中 RIP 是在 init_lsm_hooks 的 0x1cd 字节的位置,而 init_lsm_hooks 占用 0x1f0 字节的大小,它给出了一个很有用的信息,告诉我们去哪里寻找出错的代码。下文会详细介绍准确找出出错代码的过程。

而 Oops 日志中的 Code 行,会把导致 Oops 的第一条指令,也就是 RIP 的值的第一个字节,用尖括号 <> 括起来。如:

Code: 00 00 48 8b 41 10 48 89 71 20 48 8b 10 48 85 d2 75 05 eb 25 48 89 c2 48 8b 02 48 85 c0 75 f5 48 c7 01 00 00 00 00 48 89 51 08 <48> 89 0a 48 83 c1 28 48 39 f9 75 cc 31 c0 c3 48 c7 01 00 00 00 00

<48> 即是。

这种 Code 行,在没有自己编译的 vmlinux 时又想定位出错的代码行,可以利用。但是要注意 cpu 的架构问题,有些架构的(例如常见的x86)指令是不等长的;

4. Oops Error Code 解析

再看看 Error Code 行:

Oops: 0003 [#1] SMP PTI

其中 0003 为 error code,当异常发生时,由硬件压入栈中。可以通过这个看出 Oops 发生的大致原因。

对于 x86 架构来说,error code 具体定义如下:

Page fault error code bits:
bit 0 == 0: no page found 1: protection fault
bit 1 == 0: read access 1: write access
bit 2 == 0: kernel-mode access 1: user-mode access
bit 3 == 1: use of reserved bit detected
bit 4 == 1: fault was an instruction fetch

常用低 3 位,具体含义为:

  1. 如果第 0 位被清 0,则异常是由一个不存在的页所引起的;否则是由无效的访问权限引起的。
  2. 如果第 1 位被清 0,则异常由读访问或者执行访问所引起;否则异常由写访问引起。
  3. 如果第 2 位被清 0,则异常发生在内核态;否则异常发生在用户态。

所以,上述样例中的 error code 0003 表示:

  1. 异常由无效的访问权限引起,也就是说被访问的地址存在对应的物理页,但是没有权限访问;
  2. 异常由写操作引起;
  3. 异常发生在内核态,总结来说就是该异常由于在内核态对没有写权限的地址进行写操作时产生;

而 Oops 中的 [#1] 表示发生 Crash 次数。

5. 通过 gdb 定位出错的内核代码文件和行号

接下来再通过 gdb 调试做进一步动态的分析,以便确定出错的代码文件和行号。

首先需要通过 add-symbol-file 命令将符号文件添加到调试器。符号文件即通过 insmod 命令插入 LSM 模块出错时用的内核模块的 .o 文件,即日志中出现的 xxx_security。

insmod 命令:

[  415.746851] CPU: 0 PID: 8366 Comm: insmod Tainted: G           OE     4.19.82-wwh #1

xxx_security 模块:

[  415.746883]  init_module+0x34/0xc0 [xxx_security]

add-symbol-file 命令有两个参数:

  • 第一个参数是 xxx_security.o
  • 第二个参数是该模块代码正文区域的地址

该地址通过如下方式获得:

$ sudo cat /sys/module/xxx_security/sections/.text
0xffffffffc0ada000

接着通过 gdb 来调试 xxx_security.ko:

(gdb) add-symbol-file xxx_security.o 0xffffffffc0ada000
add symbol table from file "xxx_security.o" at
    .text_addr = 0xffffffffc0ada000
(y or n) yReading symbols from xxx_security.o...done.

上文已经根据 RIP 行可以得到报错函数名以及偏移:

RIP: 0010:init_lsm_hooks+0x1cd/0x1f0 [xxx_security]

接着就是反汇编 init_lsm_hooks 函数如下:

(gdb) disassemble init_lsm_hooks
Dump of assembler code for function init_lsm_hooks:
Address range 0x150 to 0x33c:
   0x0000000000000150 <+0>:	callq  0x155 <init_lsm_hooks+5>
   0x0000000000000312 <+450>:	movq   $0x0,(%rcx)
   0x0000000000000319 <+457>:	mov    %rdx,0x8(%rcx)
   0x000000000000031d <+461>:	mov    %rcx,(%rdx)
   0x0000000000000320 <+464>:	add    $0x28,%rcx
   0x0000000000000324 <+468>:	cmp    %rdi,%rcx

从上可以看出 init_lsm_hooks 函数的起始地址是 0x150,出错所在的偏移是 0x1cd。

0x150+0x1cd=0x31d,那么如何通过这个地址对应到 .c 中具体某一行呢?

(gdb) l *0x000000000000031d
0x31d is in init_lsm_hooks (./include/linux/compiler.h:220).
215	{
216		switch (size) {
217		case 1: *(volatile __u8 *)p = *(__u8 *)res; break;
218		case 2: *(volatile __u16 *)p = *(__u16 *)res; break;
219		case 4: *(volatile __u32 *)p = *(__u32 *)res; break;
220		case 8: *(volatile __u64 *)p = *(__u64 *)res; break;
221		default:
222			barrier();
223			__builtin_memcpy((void *)p, (const void *)res, size);
224			barrier();

从调试信息可以看出,出错位置为:

./include/linux/compiler.h:220 case 8: *(volatile __u64 *)p = *(__u64 *)res; break`

打开该文件以及所在行可以确认:

$ vim include/linux/compiler.h +220
static __always_inline void __write_once_size(volatile void *p, void *res, int size)
{        switch (size) {
        case 1: *(volatile __u8 *)p = *(__u8 *)res; break;
        case 2: *(volatile __u16 *)p = *(__u16 *)res; break;
        case 4: *(volatile __u32 *)p = *(__u32 *)res; break;
        case 8: *(volatile __u64 *)p = *(__u64 *)res; break;
        default:
                barrier();                __builtin_memcpy((void *)p, (const void *)res, size);
                barrier();        }}

当然,也可以通过 set listsize 20 之后看到具体的 __write_once_size 函数。该函数为 __always_inline 的,编译时被嵌入到了 init_lsm_hooks 中。

以上过程也可以通过其他方式快速定位,可进一步阅读:

  • 诊 & 断:如何快速定位 Linux Panic 出错的代码行
  • Linux Lab: 消除 qemu/raspi3 启动过程的一堆警告

6. 出错代码详细分析

上述 __write_once_size 接口是内核中所有双链表操作最终进入的函数。

在出错的 init_lsm_hooks 函数中通过调用 hlist_add_tail_rcu 调用 WRITE_ONCE, 从而进入了 __write_once_size, 接下来看看这个接口的实现:

#define WRITE_ONCE(x, val) 
({								union { typeof(x) __val; char __c[1]; } __u =	
		{ .__val = (__force typeof(x)) (val) }; 
	__write_once_size(&(x), __u.__c, sizeof(x));		__u.__val;					})

WRITE_ONCE() 用于向变量对应的内存写入值。x 对应变量,val 对应写入的值。函数首先定义并初始化一个联合体,使 __u.__val 的值为参数 val, 然后调用 __write_once_size() 函数将数据写入到内存中。

此时变量 x 所在地址 &(x) 为需要写入 val 的内存虚拟地址 ffffffffaa6f0210, 可以通过 Crash 查看该虚拟地址各项属性:

crash> vtop ffffffffaa6f0210
VIRTUAL           PHYSICAL
ffffffffaa6f0210  3faf0210
PGD DIRECTORY: ffffffffaa80a000
PAGE DIRECTORY: 3fc0e067
   PUD: 3fc0eff0 => 3fc0f063
   PMD: 3fc0fa98 => 34367063
   PTE: 34367780 => 800000003faf0061
  PAGE: 3faf0000
      PTE         PHYSICAL  FLAGS
800000003faf0061  3faf0000  (PRESENT|ACCESSED|DIRTY|NX)
      PAGE       PHYSICAL      MAppING       INDEX CNT FLAGS
ffffe02200febc00 3faf0000         0            0    1 ffffc000000800 reserved

可以看到该虚拟地址所在 PAGE 的 PTE 页表项内容,从 FLAGS 可以看出该 PAGE 不具备 R/W 属性。因而可以确认,是由于尝试向只读地址空间写数据,从而导致内核报错。

这里稍作补充,上述 PUD/PMD/PTE 信息跟第一节日志中的第二行信息一致:

PGD 3fc0e067 P4D 3fc0e067 PUD 3fc0f063 PMD 34367063 PTE 800000003faf0061

知道是在调用 hlist_add_tail_rcu 接口出错后也可以通过查看符号表得到所要写入的地址空间是否是只读的,在内核的安全模块中默认配置了 apparmor,当以 ko 的模式挂载外部的 LSM 钩子时,都需要以双链表方式挂在 apparmor 的 LSM 尾部:

$ sudo cat /proc/kallsyms | grep apparmor_hooks
ffffffffaa6eff40 r apparmor_hooks

可以看出此时 apparmor_hooks 所在内存空间是只读的。

7. 问题解决方案

进一步查看到 apparmor_hooks 的定义如下:

static struct security_hook_list apparmor_hooks[] __lsm_ro_after_init

__lsm_ro_after_init 表示 LSM 架构在完成初始化后所在的内存空间会成只读。

因此我们提出两种解决方案:

  • 解决方案1: 将 __lsm_ro_after_init 标志位去掉,让该 PAGE 可读可写,这样修改虽然可行,但不确定是否会引入其他问题。
  • 解决方案2: 可以通过内核配置添加 SECURITY_WRITABLE_HOOKS 选项,同样会将该 PAGE 配置成可读可写。

 



Tags:Oops   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
Oops 是 Linux 内核中最常见的一种异常出错情况,本文通过分析一个具体的 Oops 例子,介绍了如何分析并解决该类问题。1. LSM 内核模块出现 Oopsinsmod 一个 LSM 的 ko 模块,内...【详细内容】
2020-09-03  Tags: Oops  点击:(80)  评论:(0)  加入收藏
▌简易百科推荐
作用显示文件或目录所占用的磁盘空间使用命令格式du [option] 文件/目录命令功能显示文件或目录所占用的磁盘空间一些写法的区别du -sh xxx 显示总目录的大小,但是不会列出...【详细内容】
2021-12-23  mitsuhide1992    Tags:du命令   点击:(12)  评论:(0)  加入收藏
什么是linux内核linux就像是一个哲学的最佳实践。如果非要对它评价,我真的不知道该怎么赞叹,我只能自豪地说着:“linux的美丽简直让人沉醉。”我只能说是我处在linux学习的修炼...【详细内容】
2021-12-23  linux上的码农    Tags:linux内核   点击:(15)  评论:(0)  加入收藏
本文将比较 Linux 中 service 和 systemctl 命令,先分别简单介绍这两个命令的基础用法,然后进行比较。从 CentOS 7.x 开始,CentOS 开始使用 systemd 服务来代替 service服务(dae...【详细内容】
2021-12-23  软件架构    Tags:systemctl   点击:(13)  评论:(0)  加入收藏
mv是move的缩写,可以用来移动文件或者重命名文件名,经常用来备份文件或者目录。命令格式mv [选项] 源文件或者目录 目标文件或者目录命令功能mv命令中第二个参数类型的不同(...【详细内容】
2021-12-17  入门小站    Tags:mv命令   点击:(23)  评论:(0)  加入收藏
大数据技术AI Flink/Spark/Hadoop/数仓,数据分析、面试,源码解读等干货学习资料 98篇原创内容 -->公众号 Linux sed 命令是利用脚本来处理文本文件。sed 可依照脚本的指令来处...【详细内容】
2021-12-17  仙风道骨的宝石骑士    Tags:sed命令   点击:(21)  评论:(0)  加入收藏
Node是个啥?  写个东西还是尽量面面俱到吧,所以有关基本概念的东西我也从网上选择性地拿了下来,有些地方针对自己的理解有所改动,对这些概念性的东西有过了解的可选择跳过这段...【详细内容】
2021-12-15  linux上的码农    Tags:node   点击:(21)  评论:(0)  加入收藏
难道只有我一个人觉得Ubuntu的unity桌面非常好用吗?最近把台式机上面的Ubuntu 16.04格式化了,装了黑苹果用了一周,不得不说,MacOS确实很精美,软件生态比Linux丰富很多,比Windows简...【详细内容】
2021-12-14  地球末日村    Tags:ubuntu   点击:(34)  评论:(0)  加入收藏
简介Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。输出信息含义执行net...【详细内容】
2021-12-13  窥镜天    Tags:Linux netstat   点击:(26)  评论:(0)  加入收藏
对于较多数量的文件描述符的监听无论是select还是poll系统调用都显得捉襟见肘,poll每次都需要将所有的文件描述符复制到内核,内核本身不会对这些文件描述符加以保存,这样的设计...【详细内容】
2021-12-13  深度Linux    Tags:Linux   点击:(16)  评论:(0)  加入收藏
今天,我们来了解下 Linux 系统的革命性通用执行引擎-eBPF,之所以聊着玩意,因为它确实牛逼,作为一项底层技术,在现在的云原生生态领域中起着举足轻重的作用。截至目前,业界使用范...【详细内容】
2021-12-10  架构驿站    Tags:eBPF   点击:(24)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条