Skip to the content.

System crash caused by i40e NIC stop timeout under high PPS traffic

发表于 2025-10-02

高pps流量下i40e网卡stop超时导致的系统crash

1. 问题现象

最近QA在做性能测试,没有关闭流量的情况下,对测试设备进行了重启。刚重启没多久就出现了crash,重启后又crash。直到停止流量后,系统才正常启动。

当系统正常启动后,再开启流量测试,则工作正常。或者流量不够大,系统启动也不会crash。

也就是说问题的触发满足两个条件。

  1. 高PPS流量
  2. 系统刚启动

2. 初步分析

取coredump分析

       PANIC: "general protection fault: 0000 [#1] SMP NOPTI"
         PID: 2815
     COMMAND: "ifconfig"
        TASK: ffffa15a928af000  [THREAD_INFO: ffffa15a928af000]
         CPU: 5
       STATE: TASK_RUNNING (PANIC)

“general protection fault: 0000 [#1] SMP NOPTI” 这种错误一般都是 异常地址访问触发访问保护。我们查看对应的异常地址

crash> bt
PID: 2815     TASK: ffffa15a928af000  CPU: 5    COMMAND: "ifconfig"
 #0 [ffffa44b5bc4fba8] machine_kexec at ffffffff9743e54c
 #1 [ffffa44b5bc4fbf8] __crash_kexec at ffffffff974cbd3b
 #2 [ffffa44b5bc4fcd0] oops_end at ffffffff9741c009
 #3 [ffffa44b5bc4fcf8] die at ffffffff9741c083
 #4 [ffffa44b5bc4fd28] do_general_protection at ffffffff9741a45e
 #5 [ffffa44b5bc4fd50] general_protection at ffffffff97e01325
    [exception RIP: acct_collect+92]
    RIP: ffffffff974cb68c  RSP: ffffa44b5bc4fe08  RFLAGS: 00010202
    RAX: 004500087d7dca0b  RBX: ffffa15a92b28400  RCX: 0000000000000000
    RDX: 90003f1058ecf300  RSI: 0000000000000001  RDI: ffffa15a75a90d40
    RBP: ffffa44b5bc4fe28   R8: 0000000000000000   R9: 00007f8e150a58d8
    R10: 0000000000000000  R11: 0000000000000000  R12: 0000000000000000
    R13: 0000000000000000  R14: ffffa15a928af000  R15: 0000000000000000
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
 #6 [ffffa44b5bc4fe30] do_exit at ffffffff97456837
 #7 [ffffa44b5bc4fea0] do_group_exit at ffffffff974571db
 #8 [ffffa44b5bc4fed0] sys_exit_group at ffffffff97457264
 #9 [ffffa44b5bc4fee0] do_syscall_64 at ffffffff974016d5
#10 [ffffa44b5bc4ff50] entry_SYSCALL_64_after_hwframe at ffffffff97e0008d

crash> dis -l acct_collect+92
/root/linux-4.14.336/kernel/acct.c: 547
0xffffffff974cb68c <acct_collect+92>:   add    0x8(%rax),%rdx    //访问rax寄存器

因为 rax = 004500087d7dca0b 不是一个正常的地址,所以导致 “general protection fault”。 那么rax是什么,从哪里来?

image

crash> dis -r acct_collect+92
0xffffffff974cb630 <acct_collect>:      nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffff974cb635 <acct_collect+5>:    push   %rbp
0xffffffff974cb636 <acct_collect+6>:    mov    %rsp,%rbp
0xffffffff974cb639 <acct_collect+9>:    push   %r14
0xffffffff974cb63b <acct_collect+11>:   push   %r13
0xffffffff974cb63d <acct_collect+13>:   mov    %gs:0x19dc0,%r14    //current指向的当前task_struct
0xffffffff974cb646 <acct_collect+22>:   push   %r12
0xffffffff974cb648 <acct_collect+24>:   mov    %rdi,%r12
0xffffffff974cb64b <acct_collect+27>:   push   %rbx
0xffffffff974cb64c <acct_collect+28>:   mov    0x650(%r14),%rbx
0xffffffff974cb653 <acct_collect+35>:   test   %esi,%esi
0xffffffff974cb655 <acct_collect+37>:   je     0xffffffff974cb7a2 <acct_collect+370>
0xffffffff974cb65b <acct_collect+43>:   xor    %r13d,%r13d
0xffffffff974cb65e <acct_collect+46>:   cmpq   $0x0,0x3a0(%r14)
0xffffffff974cb666 <acct_collect+54>:   je     0xffffffff974cb6bc <acct_collect+140>
0xffffffff974cb668 <acct_collect+56>:   mov    0x3a0(%r14),%rax
0xffffffff974cb66f <acct_collect+63>:   lea    0x80(%rax),%rdi
0xffffffff974cb676 <acct_collect+70>:   call   0xffffffff97cde3e0 <down_read>
0xffffffff974cb67b <acct_collect+75>:   mov    0x3a0(%r14),%rax  //task_struct.mm
0xffffffff974cb682 <acct_collect+82>:   mov    (%rax),%rax
0xffffffff974cb685 <acct_collect+85>:   test   %rax,%rax
0xffffffff974cb688 <acct_collect+88>:   je     0xffffffff974cb6a3 <acct_collect+115>
0xffffffff974cb68a <acct_collect+90>:   xor    %edx,%edx
0xffffffff974cb68c <acct_collect+92>:   add    0x8(%rax),%rdx

crash> task_struct.mm -ox
struct task_struct {
   [0x3a0] struct mm_struct *mm;
}
crash> mm_struct.mmap -ox
struct mm_struct {
    [0x0] struct vm_area_struct *mmap;
}

配合代码和反汇编,我们知道 rax是 vm_area_struct *vma指针。

crash> task_struct.mm -x ffffa15a928af000
  mm = 0xffffa15a75a90cc0,
crash> mm_struct.mmap -x 0xffffa15a75a90cc0
  mmap = 0xffffa15a4533c2e0
crash> vm_area_struct.vm_next -ox
struct vm_area_struct {
  [0x10] struct vm_area_struct *vm_next;
}
crash> list -o 0x10 0xffffa15a4533c2e0  //循环查看所有vm_area_struct
ffffa15a4533c2e0
ffffa15a4533c228
ffffa15a4533c170
ffffa15a4533c0b8
4500087d7dca0b      //出问题的vm_area_struct
list: invalid kernel virtual address: 4500087d7dca1b  type: "list entry"

可以看到其中 vm_area_struct ffffa15a4533c0b8 的 vm_next是个异常的值, 导致while (vma) 遍历操作访问 vma->end时出错。

crash> kmem ffffa15a4533c0b8
CACHE             OBJSIZE  ALLOCATED     TOTAL  SLABS  SSIZE  NAME
ffffa15a93632300      184       1897      4246    193     4k  vm_area_struct
  SLAB              MEMORY            NODE  TOTAL  ALLOCATED  FREE
  ffffe1932014cf00  ffffa15a4533c000     0     22         22     0
  FREE / [ALLOCATED]
  [ffffa15a4533c0b8]

      PAGE        PHYSICAL      MAPPING       INDEX CNT FLAGS
ffffe1932014cf00 80533c000                0        0  1 8000000000000100 slab

crash> vm_area_struct -x ffffa15a4533c0b8
struct vm_area_struct {
  vm_start = 0x48c000,
  vm_end = 0x90003f10592d0300,
  vm_next = 0x4500087d7dca0b,
  vm_prev = 0x11ff0000ca1f2e00,
  vm_rb = {
    __rb_parent_color = 0x101030101012734,
    rb_right = 0x1a005000ec9ac964,
    rb_left = 0x0
  },
  rb_subtree_gap = 0x0,
  vm_mm = 0xffffa15a00000000,       //一半被写坏
  vm_page_prot = {
    pgprot = 0x8000000000000025
  },
  vm_flags = 0x100871,
  shared = {
    rb = {
      __rb_parent_color = 0xffffa15a453bb9b0,
      rb_right = 0x0,
      rb_left = 0x0
    },
    rb_subtree_last = 0x8c
  },
  anon_vma_chain = {
    next = 0xffffa15a45208c50,
    prev = 0xffffa15a45208c50
  },
  anon_vma = 0xffffa15a44847a00,
  vm_ops = 0xffffffff98227480 <ext4_file_vm_ops>,
  vm_pgoff = 0x8b,
  vm_file = 0xffffa15a45352000,
  vm_private_data = 0x0,
  swap_readahead_info = {
    counter = 0x0
  },
  vm_userfaultfd_ctx = {<No data fields>}
}

可以看到 ffffa15a4533c0b8 是被slab申请用作 vm_area_struct, 但是vm_mm之前的内存被写坏。

初步判定是内存被异常覆写。是UAF(use-after-free)么?谁覆写的?

3. 深入挖掘

当我盯着异常数据看的时候,”vm_next = 0x4500087d7dca0b”中0x45这个数字特别的醒目。直觉告诉我这可能是个IP header数据。经常分析报文的都知道IP头部第一个字节值就是0x45。 于是我用ip_hdr试着读取。

crash> rd ffffa15a4533c0b8 23  
ffffa15a4533c0b8:  000000000048c000 90003f10592d0300   ..H.......-Y.?..
ffffa15a4533c0c8:  004500087d7dca0b 11ff0000ca1f2e00   ..}}..E.........
ffffa15a4533c0d8:  0101030101012734 1a005000ec9ac964   4'......d....P..
ffffa15a4533c0e8:  0000000000000000 0000000000000000   ................
ffffa15a4533c0f8:  ffffa15a00000000 8000000000000025   ....Z...%.......
ffffa15a4533c108:  0000000000100871 ffffa15a453bb9b0   q.........;EZ...
ffffa15a4533c118:  0000000000000000 0000000000000000   ................
ffffa15a4533c128:  000000000000008c ffffa15a45208c50   ........P. EZ...
ffffa15a4533c138:  ffffa15a45208c50 ffffa15a44847a00   P. EZ....z.DZ...
ffffa15a4533c148:  ffffffff98227480 000000000000008b   .t".............
ffffa15a4533c158:  ffffa15a45352000 0000000000000000   . 5EZ...........
ffffa15a4533c168:  0000000000000000                    ........

crash> iphdr -x ffffa15a4533c0ce
struct iphdr {
  ihl = 0x5,
  version = 0x4,
  tos = 0x0,
  tot_len = 0x2e00,
  id = 0xca1f,
  frag_off = 0x0,
  ttl = 0xff,
  protocol = 0x11,       //UDP
  check = 0x2734,
  saddr = 0x3010101,       //1.1.1.3
  daddr = 0xc9640101       //1.1.100.201
}
crash> net -N 0x3010101
1.1.1.3
crash> net -N 0xc9640101
1.1.100.201

确实是IP/UDP报文。 也就是说某个处理中把 vm_area_struct内存当做skb的buffer操作了。

[   24.702850] 8021q: adding VLAN 0 to HW filter on device eth9
[   24.703387] IPv6: ADDRCONF(NETDEV_CHANGE): eth9: link becomes ready
[   24.761472] i40e 0000:07:00.3: VSI seid 399 Rx ring 0 disable timeout
[   24.839959] i40e 0000:07:00.3 eth9: NIC Link is Down
[   24.845870] 8021q: adding VLAN 0 to HW filter on device eth10
[   24.906828] i40e 0000:07:00.2: VSI seid 398 Rx ring 0 disable timeout
[   24.985357] i40e 0000:07:00.2 eth10: NIC Link is Down
[   24.991310] 8021q: adding VLAN 0 to HW filter on device eth11
[   24.991814] general protection fault: 0000 [#1] SMP NOPTI

crash之前有几个接口 up/down操作。eth9和eth10都出现了 “Rx ring disable timeout” 。 查看代码这是

static int i40e_vsi_control_rx(struct i40e_vsi *vsi, bool enable)
{
	struct i40e_pf *pf = vsi->back;
	int i, pf_q, ret = 0;

	pf_q = vsi->base_queue;
	for (i = 0; i < vsi->num_queue_pairs; i++, pf_q++) {
		ret = i40e_control_wait_rx_q(pf, pf_q, enable);
		if (ret) {
			dev_info(&pf->pdev->dev,
				 "VSI seid %d Rx ring %d %sable timeout\n",              //这里打印
				 vsi->seid, pf_q, (enable ? "en" : "dis"));
			break;
		}
	}

	/* HW needs up to 50ms to finish RX queue disable*/
	if (!enable)
		mdelay(50);

	return ret;
}

int i40e_control_wait_rx_q(struct i40e_pf *pf, int pf_q, bool enable)
{
	int ret = 0;

	i40e_control_rx_q(pf, pf_q, enable);

	/* wait for the change to finish */
	ret = i40e_pf_rxq_wait(pf, pf_q, enable);       //这里返回错误
	if (ret)
		return ret;

	return ret;
}

static void i40e_control_rx_q(struct i40e_pf *pf, int pf_q, bool enable)
{
	struct i40e_hw *hw = &pf->hw;
	u32 rx_reg;
	int i;

	for (i = 0; i < I40E_QTX_ENA_WAIT_COUNT; i++) {
		rx_reg = rd32(hw, I40E_QRX_ENA(pf_q));
		if (((rx_reg >> I40E_QRX_ENA_QENA_REQ_SHIFT) & 1) ==
		    ((rx_reg >> I40E_QRX_ENA_QENA_STAT_SHIFT) & 1))
			break;
		usleep_range(1000, 2000);
	}

	/* Skip if the queue is already in the requested state */
	if (enable == !!(rx_reg & I40E_QRX_ENA_QENA_STAT_MASK))
		return;

	/* turn on/off the queue */
	if (enable)
		rx_reg |= I40E_QRX_ENA_QENA_REQ_MASK;
	else
		rx_reg &= ~I40E_QRX_ENA_QENA_REQ_MASK;

	wr32(hw, I40E_QRX_ENA(pf_q), rx_reg);              //disable rx queue
}

static int i40e_pf_rxq_wait(struct i40e_pf *pf, int pf_q, bool enable)
{
	int i;
	u32 rx_reg;

	for (i = 0; i < I40E_QUEUE_WAIT_RETRY_LIMIT; i++) {       //循环I40E_QUEUE_WAIT_RETRY_LIMIT = 10 次
		rx_reg = rd32(&pf->hw, I40E_QRX_ENA(pf_q));       //读取寄存器
		if (enable == !!(rx_reg & I40E_QRX_ENA_QENA_STAT_MASK))       //下发成功则跳出
			break;

		usleep_range(10, 20);       //每次等待10~20微秒
	}
	if (i >= I40E_QUEUE_WAIT_RETRY_LIMIT)
		return -ETIMEDOUT;       //超时返回错误

	return 0;
}

这个错误log表示 接收DMA队列关闭失败,即DMA仍在工作。随后的操作会释放DMA内存。

i40e_down --> i40e_vsi_stop_rings --> i40e_vsi_control_rx --> i40e_control_rx_q	//停止DMA队列
          |--> i40e_clean_rx_ring --> 						//释放DMA内存

这样会造成 DMA队列还在工作,但是DMA内存可能已经被其他地方申请使用了。

4. 解决方案

4.1 方案1

为了防止这个问题发生,可以开启IOMMU。要确认CPU支持VT-d。

  • BIOS中启用VT-d。(有的默认开启,有的需要手动开启)
  • .config中打开 CONFIG_INTEL_IOMMU=y
  • 启动命令中 intel_iommu=on

我们产品BIOS中显示已支持VT-d,但是没有开关选项,所以不确认是否开启。 编译image后,发现 dmar_tbl获取不到。

int __init detect_intel_iommu(void)
{
	int ret;
	struct dmar_res_callback validate_drhd_cb = {
		.cb[ACPI_DMAR_TYPE_HARDWARE_UNIT] = &dmar_validate_one_drhd,
		.ignore_unhandled = true,
	};

	down_write(&dmar_global_lock);
	ret = dmar_table_detect();		//获取dmar_tbl
	if (!ret)
		ret = dmar_walk_dmar_table((struct acpi_table_dmar *)dmar_tbl,
					   &validate_drhd_cb);
	if (!ret && !no_iommu && !iommu_detected && !dmar_disabled) {
		iommu_detected = 1;
		/* Make sure ACS will be enabled */
		pci_request_acs();
	}

#ifdef CONFIG_X86
	if (!ret)
		x86_init.iommu.iommu_init = intel_iommu_init;
#endif

	if (dmar_tbl) {
		acpi_put_table(dmar_tbl);
		dmar_tbl = NULL;
	}
	up_write(&dmar_global_lock);

	return ret ? ret : 1;
}

/**
 * dmar_table_detect - checks to see if the platform supports DMAR devices
 */
static int __init dmar_table_detect(void)
{
	acpi_status status = AE_OK;

	/* if we could find DMAR table, then there are DMAR devices */
	status = acpi_get_table(ACPI_SIG_DMAR, 0, &dmar_tbl);	//失败,dmar_tbl地址为0

	if (ACPI_SUCCESS(status) && !dmar_tbl) {
		pr_warn("Unable to map DMAR\n");
		status = AE_NOT_FOUND;
	}

	return ACPI_SUCCESS(status) ? 0 : -ENOENT;
}

已就该问题向intel发邮件请求分析。

4.2 方案2

因为时间急, 所以转而寻找第二方案。 于是决定增加查询rx队列状态的等待时间。

#define I40E_QUEUE_WAIT_RETRY_LIMIT	100000	//默认10,该到了100000
static int i40e_pf_rxq_wait(struct i40e_pf *pf, int pf_q, bool enable)
{
	int i;
	u32 rx_reg;

	for (i = 0; i < I40E_QUEUE_WAIT_RETRY_LIMIT; i++) {
		rx_reg = rd32(&pf->hw, I40E_QRX_ENA(pf_q));
		if (enable == !!(rx_reg & I40E_QRX_ENA_QENA_STAT_MASK))
			break;

		usleep_range(10, 20);
	}
	if (i >= I40E_QUEUE_WAIT_RETRY_LIMIT)
		return -ETIMEDOUT;

	return 0;
}

将I40E_QUEUE_WAIT_RETRY_LIMIT改为100000后,最大等待时间

经验证,问题不再出现,log显示最长循环约50000次,等待在1s左右。

这样虽然没有方案1优雅,但短时间也只能选择这个方案。

4.3 方案3

因为觉得方案2有点不优雅。想从触发问题的诱因出发,看看有什么办法修复。

追溯进程信息,发现调用链条

1:init –> 2093:smit –> 2094:cmdbsvr –> 2301:sh –> 2302:dev_bind_irq –> 2815:ifconfig

crash> ps
//部分省略
        1       0   2  ffffa15a93660000  IN   0.0    19560     8632  init
     2093       1   6  ffffa15a42ebf000  IN   0.0    20776    11564  smit
     2094    2093   4  ffffa15a42f7d400  IN   0.1    82292    34452  cmdbsvr
     2301    2094   6  ffffa15a928ff000  IN   0.0    16464     7900  sh
     2302    2301   2  ffffa15a9273d400  IN   0.0    16464     7884  dev_bind_irq
>    2815    2302   5  ffffa15a928af000  RU   0.0    16464     7616  ifconfig

经查看 dev_bind_irq是一个脚本,用来优化 网卡设备的中断绑定的。它在这个阶段会对网卡执行up/down并调整中断绑定。 也就是这个up/down导致了问题的发生。

经分析,i40e的中断收发队列是默认每个cpu一个,很均匀。并不需要在这优化。 时间紧急为了最小化影响,就只将i40e的dev_bind_irq操作取消。

经测试问题不在出现。版本即将发布,这个workaround能保证在不引入潜在新问题的情况下,修复这个crash问题。

5. 后记

待和intel确认dmar_tbl问题后,还是会使用更优雅的方案1。

本文访问次数:...