一 问题描述
某行v7服务器出现异常重启现象,故障系统转储vmcore,以及宕机的内核日志。
二 日志分析
通过查看dmesg日志,报错日志可分为两类:
1 python脚本运行过程中出现的”unhandled level 3 translation fault”,属于用户层进程崩溃的错误,和宕机问题无关;
2 时间戳在61817741时,内核报错“IPv4: Attempt to release TCP socket in state 10 ffff805624a7b800”。同时出现“Unable to handle kernel paging request at virtual address 0001950b”的报错,说明内核运行过程中访问了无效的虚拟地址或者说野指针。
2-1 分析”IPv4: Attempt to release TCP socket in state 10”报错
针对“IPv4: Attempt to release TCP socket in state 10 ffff805624a7b800”报错,内核函数(基于4.14内核)为inet_sock_destruct具体代码如图一:
图 一
该函数核心作用为当用户层close socket时,释放对应inet路由缓存。根据代码逻辑可知,当sk->sk_state不为TCP_CLOSE时,会打印错误信息,同时输出对应sk->sk_state。由内核日志可知出问题时状态为10,通过图二TCP状态值枚举结构可知,当时sk_state为TCP_LISTEN。
图 二
所以此处存在一个sk_state异常的问题,此处可能存在某种TCP通信过程中应用established->close->建立socket->listen的过程,close状态还未更新,sock已经处于listen状态。
2-2 分析内核oops
报错之后的堆栈信息如图三:
图 三
堆栈表示mlx5网卡驱动mlx5e_poll_rx_cq接受到网卡缓冲区的数据,通过__netif_receive_skb将sk_buffer提交到协议层,ip_local_deliver_finish在ip层做路由选择后提交给TCP层,入口函数为tcp_v4_rcv。
图四为tcp_v4_rcv->__inet_lookup_listener流程:
图 四
通过以上流程图可知,__inet_lookup_skb实际为__inet_lookup_established和__inet_lookup_listener的封装。__inet_lookup_established核心代码如图五:
图 五
核心功能为通过sk_nulls_for_each_rcu遍历established sock的hash表,选出匹配到对应源目的IP和源目的端口的成员,并返回对应sk结构指针。当sk->sk_refcnf引用计数为0时则返回NULL。此时就会转而执行__inet_lookup_listener,函数如图六:
图 六
核心功能为通过sk_for_each_rcu遍历listener sock的hash表,通过resueport_select_sock选择合适的可复用端口,返回对应sk结构指针。
通过crash调试vmcore:
通过dis ffff00000873ce54(__inet_lookup_listener+0xac)获取函数符号地址的反汇编信息,同时通过sym获取__inet_lookup_listener的符号地址如图七:
图 七
通过图七可知,内核崩溃的原因在于将x19寄存器48偏移位置的值传入到x3寄存器时,该传入值是一个无效地址。
通过dis -l __inet_lookup_listener(符号地址)获取函数的反汇编信息如图八:
图 八
通过图八可知,出问题的代码在net/ipv4/inet_hashtables.c的178行,查看内核源码可知该代码在compute_score函数中,同时也可以看到后续178行cmp的指令,说明存在对应条件比较。
下面为对应源码文件171-179行内容:
图 九
net_eq比较sock_net(sk)返回地址和net地址对应cmp指令,所以问题很可能出现在sock_net(sk)函数内部。sock_net函数如图十:
图 十
函数主要获取sk->sk_net成员的地址(即x19寄存器偏移48后的地址)。现在只需要确认sk_net成员在sock结构体中的偏移是否为48。
图十一为sock结构布局:
图 十一
136偏移之前都是在sock_common结构中,查看sock_common结构布局如图十二:
图 十二
结合内核源码图十三:
图 十三
到此可以确认代码调用至__inet_lookup_listener->compute_socre->sock_net时
获取sk->sk_net地址时访问了无效地址。
2-3 分析sk初始化的问题
在2-2图四中已知,tcp_v4_rcv函数属于tcp层的入口函数。会存在__inet_lookup_established和__inet_lookup_listener两处查询函数的原因在于:内核需要区别此次通信是各个进程的第一次握手,还是建立三次握手后的通信。但是在两个函数遍历查询sock的方式存在差异(4.14针对ipv4而言):
上图的核心差异在于listen hash表和established hash表插入链表的方式不一样,hlist_nulls_add_tail_rcu初始化链表过程会检查各node的地址(sk)是否为NULL, hlist_add_head_rcu 则不会检查。
__inet_lookup_listener通过sk_for_each_rcu遍历listener 表时存在null的sock结构后,如果sk已经为空,则会出现访问非法地址的情况,为了避免出现此类问题,需要在 sock加入链表前进行有效性判断。
结合2-1的报错,当时close触发后并没有触发close状态更新,对应sock的状态直接变为listen,会被重新添加到对应listener hash表中,此时sock的指针可能已经是无效的地址。
针对该问题社区已经提供对应解决方案:
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=v4.19.219&id=28f0d54dbed848d231c9af37737ddd00968caaac