[crash分析]因为HOOK okfn导致的NULL pointer dereference

设备启用业务模块,同时又创建了桥口转发docker业务。在调试过程中,出现crash。

[  794.020484] BUG: unable to handle kernel
[  794.022499] NULL pointer dereference at 0000000000000006
[  794.024455] IP: [<ffffffffa0330ee3>] br_nf_pre_routing_finish+0x53/0x450 [br_netfilter]
...	//省略无关
[  794.101362]  [<ffffffff815aee4c>] ? __ip_route_output_key_hash+0x32c/0x930
[  794.103338]  [<ffffffffa0750bbc>] Test_LocalDeliver+0xfc/0x260 [testmod]
[  794.105446]  [<ffffffffa06b8663>] _Test_DoActions+0x483/0x1060 [testmod]
[  794.107566]  [<ffffffffa033b33d>] ? nf_conntrack_free+0x4d/0x60 [nf_conntrack]
[  794.109725]  [<ffffffffa033c9b0>] ? destroy_conntrack+0xa0/0x100 [nf_conntrack]
[  794.111876]  [<ffffffffa0093da3>] ? start_xmit+0x253/0x4f0 [virtio_net]
[  794.113901]  [<ffffffffa06becdd>] ? LW_FlowTableLookup+0x1ad/0x950 [testmod]
[  794.115942]  [<ffffffffa06ba1db>] Test_Process+0x85b/0xea0 [testmod]

查看异常栈,crash在br_nf_pre_routing_finish+0x53/0x450位置,查看反汇编。

crash> dis -r br_nf_pre_routing_finish+0x53
0xffffffffa0330e90 <br_nf_pre_routing_finish>:  nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffffa0330e95 <br_nf_pre_routing_finish+5>:        push   %rbp
0xffffffffa0330e96 <br_nf_pre_routing_finish+6>:        mov    %rsp,%rbp
0xffffffffa0330e99 <br_nf_pre_routing_finish+9>:        push   %r15
0xffffffffa0330e9b <br_nf_pre_routing_finish+11>:       push   %r14
0xffffffffa0330e9d <br_nf_pre_routing_finish+13>:       mov    %rdi,%r14
0xffffffffa0330ea0 <br_nf_pre_routing_finish+16>:       push   %r13
0xffffffffa0330ea2 <br_nf_pre_routing_finish+18>:       push   %r12
0xffffffffa0330ea4 <br_nf_pre_routing_finish+20>:       push   %rbx
0xffffffffa0330ea5 <br_nf_pre_routing_finish+21>:       mov    %rsi,%rbx
0xffffffffa0330ea8 <br_nf_pre_routing_finish+24>:       sub    $0x90,%rsp
0xffffffffa0330eaf <br_nf_pre_routing_finish+31>:       mov    0x20(%rsi),%r13
0xffffffffa0330eb3 <br_nf_pre_routing_finish+35>:       mov    0x90(%rsi),%r12	//r12 = [rsi + 0x90]
0xffffffffa0330eba <br_nf_pre_routing_finish+42>:       mov    %gs:0x28,%rax
0xffffffffa0330ec3 <br_nf_pre_routing_finish+51>:       mov    %rax,-0x30(%rbp)
0xffffffffa0330ec7 <br_nf_pre_routing_finish+55>:       xor    %eax,%eax
0xffffffffa0330ec9 <br_nf_pre_routing_finish+57>:       movzwl 0x3a(%rsi),%eax
0xffffffffa0330ecd <br_nf_pre_routing_finish+61>:       movzwl 0xc2(%rsi),%r15d
0xffffffffa0330ed5 <br_nf_pre_routing_finish+69>:       mov    0xe0(%rsi),%rcx
0xffffffffa0330edc <br_nf_pre_routing_finish+76>:       mov    0x3e8(%r13),%r9
0xffffffffa0330ee3 <br_nf_pre_routing_finish+83>:       mov    %ax,0x6(%r12)	//问题位置

查看问题指令可知 r12寄存器为0,对r12 指向地址偏移6位置的访问导致异常地址访问。r12是skb(
rsi)偏移0x90得到。可知 r12是skb->nf_bridge。也就是说skb->nf_bridge 为NULL导致了问题发生。
查看代码 br_nf_pre_routing_finish 是注册在bridge hook的 NF_BR_PRE_ROUTING链上的br_nf_pre_routing中的处理。从代码中看nf_bridge不应该为NULL的。

static unsigned int br_nf_pre_routing(const struct nf_hook_ops *ops,
          struct sk_buff *skb,
          const struct net_device *in,
          const struct net_device *out,
          const struct nf_hook_state *state)
{
...//省略部分
 if (!nf_bridge_alloc(skb))
  return NF_DROP;
  ...//省略部分
   nf_bridge = nf_bridge_info_get(skb);
   nf_bridge->ipv4_daddr = ip_hdr(skb)->daddr;
   
  skb->protocol = htons(ETH_P_IP);
  NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, state->sk, skb,
    skb->dev, NULL, br_nf_pre_routing_finish);
  return NF_STOLEN;
}

随后将疑点转向调用栈,发现业务模块 Test_LocalDeliver–>br_nf_pre_routing_finish。查看Test_LocalDeliver函数,发现是通过调用保存的L3RxOkfn来将数据包方向本机。
这里明明保存的三层的okfn,怎么调用到二层hook链上的okfn了?
(这里简单介绍下L3RxOkfn,我们在三层NF_INET_PRE_ROUTING注册了hook,将包收上来后,将对应的okfn保存到L3RxOkfn中,随后将包stolen,放到业务模块的链上。在task调度中,业务模块将包从链上取出进行处理后,发向本机的包,通过L3RxOkfn来继续处理)。

正常情况下我们在三层的hook抓包后,L3RxOkfn应该是ip_rcv_finish

ip_rcv 函数的末尾
 return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, NULL, skb,
         dev, NULL,
         ip_rcv_finish);

br_nf_pre_routing的末尾
 NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, state->sk, skb,
  skb->dev, NULL,
  br_nf_pre_routing_finish);

分析到这里,问题基本清晰了。

  1. 某个数据包在bridge接口的 PREROUTING中处理,进入了br_nf_pre_routing,然后进入了 NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, state->sk, skb, skb->dev, NULL,br_nf_pre_routing_finish)。
  2. 进入NF_INET_PREROUTING后,被业务模块三层hook钩住, 会更新 L3RxOkfn = br_nf_prerouting_finish。
  3. 此后tasklet调度到业务模块,处理完送到本机的包(这是普通的三层包,没有nfbridge),调用gsL3RxOkfn 出现crash。

这个问题是概率出现,因为在tasklet调度到业务模块之前,有新包在三层PREROUTINGhook处理,会将L3RxOkfn 重新更新成ip_rcv_finish,这样后续处理就没问题了。

如果不想要二层birdge桥口hook处理中调用三层netfilter call tables。可以echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables 关闭。.

发布了27 篇原创文章 · 获赞 2 · 访问量 4603

猜你喜欢

转载自blog.csdn.net/xqjcool/article/details/105087948