LinuxカーネルHack: ICMPエコー発、procファイルシステム着 (gdbのwatchを使うよ)
はじめに
前回に引き続き、ICMPまわりを追っていこうとしたら、大幅に脱線して、procファイルシステムに到着。
カーネルを少しいじりつつ、gdbの変数監視機能(watch)を利用することで、/proc/sys/net/ipv4/icmp_echo_ignore_allへの書き込みを監視することに成功した。
脱線のへの道のり
ICMPエコーはicmp_echo関数によって処理される。
icmp_echo関数を見ると、net->ipv4.sysctl_icmp_echo_ignore_allフラグが立っていない場合は、
ICMPエコーを返さないようだ。
% cat net/ipv4/icmp.c [...] /* * Handle ICMP_ECHO ("ping") requests. * * RFC 1122: 3.2.2.6 MUST have an echo server that answers ICMP echo * requests. * RFC 1122: 3.2.2.6 Data received in the ICMP_ECHO request MUST be * included in the reply. * RFC 1812: 4.3.3.6 SHOULD have a config option for silently ignoring * echo requests, MUST have default=NOT. * See also WRT handling of options once they are done and working. */ static void icmp_echo(struct sk_buff *skb) { struct net *net; net = dev_net(skb_dst(skb)->dev); if (!net->ipv4.sysctl_icmp_echo_ignore_all) { struct icmp_bxm icmp_param; icmp_param.data.icmph = *icmp_hdr(skb); icmp_param.data.icmph.type = ICMP_ECHOREPLY; icmp_param.skb = skb; icmp_param.offset = 0; icmp_param.data_len = skb->len; icmp_param.head_len = sizeof(struct icmphdr); icmp_reply(&icmp_param, skb); } }
ここで、net->ipv4.sysctl_icmp_echo_ignore_allフラグが気になった。
どこでこのフラグを制御しているのだろう?
Emacsに設定したGNU Globalで"sysctl_icmp_echo_ignore_all"を調査。
以下の候補が見つかった。
sysctl_icmp_echo_ignore_all 49 include/net/netns/ipv4.h int sysctl_icmp_echo_ignore_all; sysctl_icmp_echo_ignore_all 832 net/ipv4/icmp.c if (!net->ipv4.sysctl_icmp_echo_ignore_all) { sysctl_icmp_echo_ignore_all 1180 net/ipv4/icmp.c net->ipv4.sysctl_icmp_echo_ignore_all = 0; sysctl_icmp_echo_ignore_all 630 net/ipv4/sysctl_net_ipv4.c .data = &init_net.ipv4.sysctl_icmp_echo_ignore_all, sysctl_icmp_echo_ignore_all 698 net/ipv4/sysctl_net_ipv4.c &net->ipv4.sysctl_icmp_echo_ignore_all;
これが一番怪しい。
% cat net/ipv4/sysctl_net_ipv4.c [...] static struct ctl_table ipv4_net_table[] = { { .procname = "icmp_echo_ignore_all", .data = &init_net.ipv4.sysctl_icmp_echo_ignore_all, .maxlen = sizeof(int), .mode = 0644, .proc_handler = proc_dointvec },
"icmp_echo_ignore_all"でネットで検索してみると、以下のページが見つかった。
http://www.linux.or.jp/JF/JFdocs/Adv-Routing-HOWTO/lartc.kernel.obscure.html
なるほど、procファイルシステムから制御できるのか。
試してみる。デフォルトは0と。
(uml)# cat /proc/sys/net/ipv4/icmp_echo_ignore_all 0
icmp_echo_ignore_allが0の時は、応答が返ってくる。
% ping 192.168.10.1 PING 192.168.10.1 (192.168.10.1) 56(84) bytes of data. 64 bytes from 192.168.10.1: icmp_seq=1 ttl=64 time=0.947 ms 64 bytes from 192.168.10.1: icmp_seq=2 ttl=64 time=0.333 ms 64 bytes from 192.168.10.1: icmp_seq=3 ttl=64 time=0.312 ms 64 bytes from 192.168.10.1: icmp_seq=4 ttl=64 time=0.291 ms 64 bytes from 192.168.10.1: icmp_seq=5 ttl=64 time=0.288 ms
icmp_echo_ignore_allを1にしてみる。
(uml)# echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
今度は、応答が返ってこなくなった。
% ping 192.168.10.1 PING 192.168.10.1 (192.168.10.1) 56(84) bytes of data.
ちなみに、GNU Globalでヒットしたicmp_sk_init関数を見ると、net->ipv4.sysctl_icmp_echo_ignore_allを0
に初期化していることがわかる。icmp_sk_initは興味が無いので置いておく。
% cat net/ipv4/icmp.c static int __net_init icmp_sk_init(struct net *net) { [...] /* Control parameters for ECHO replies. */ net->ipv4.sysctl_icmp_echo_ignore_all = 0;
gccの最適化問題への対処
icmp_echoにある、net->ipv4.sysctl_icmp_echo_ignore_allの書き込みをgdbで監視することで、
どのようなパスでこの変数に書き込みが発生しているのか解明したい。
icmp_echoでブレークして、pingを打つ。
(gdb) b icmp_echo Breakpoint 2 at 0x819803e: file net/ipv4/icmp.c, line 832. (gdb) c
icmp_echoでブレークした。
Breakpoint 2, icmp_echo (skb=0x9c7a080) at net/ipv4/icmp.c:832 832 if (!net->ipv4.sysctl_icmp_echo_ignore_all) { (gdb) bt #0 icmp_echo (skb=0x9c7a080) at net/ipv4/icmp.c:832 #1 0x08197a3d in icmp_rcv (skb=0x9c7a080) at net/ipv4/icmp.c:1053 #2 0x0817910b in ip_local_deliver (skb=0x9c7a080) at net/ipv4/ip_input.c:226 #3 0x081795bf in ip_rcv (skb=0x9c7a080, dev=0x9ee9c00, pt=0x8234178, orig_dev=0x9ee9c00) at include/net/dst.h:317 #4 0x0816348d in __netif_receive_skb (skb=0x9c7a080) at net/core/dev.c:2931 #5 0x08165e2e in process_backlog (napi=0x8235e40, quota=15) at net/core/dev.c:3368 #6 0x08165f0f in net_rx_action (h=0x823c890) at net/core/dev.c:3526 #7 0x08075936 in __do_softirq () at kernel/softirq.c:219 #8 0x080759e4 in do_softirq () at kernel/softirq.c:266 #9 0x08075aa2 in irq_exit () at kernel/softirq.c:303 #10 0x08057fbb in do_IRQ (irq=5, regs=0x8223c78) at arch/um/kernel/irq.c:338 #11 0x080580a9 in sigio_handler (sig=29, regs=0x8223c78) at arch/um/kernel/irq.c:97 #12 0x08065bc6 in sig_handler_common (sig=29, sc=0x8223d24) at arch/um/os-Linux/signal.c:49 #13 0x08065eb5 in sig_handler (sig=8, sc=0x8223d24) at arch/um/os-Linux/signal.c:81 #14 0x08065e14 in handle_signal (sig=164077696, sc=0x8223d24) at arch/um/os-Linux/signal.c:158 #15 0x0806754b in hard_handler (sig=29) at arch/um/os-Linux/sys-i386/signal.c:12 #16 <signal handler called> Backtrace stopped: previous frame inner to this frame (corrupt stack?)
netの値を見てみる。しかし、シンボルが無いと怒られた。
おそらく、gccの最適化によって、netシンボルが消えてしまったと考えられる。
(gdb) l 827 static void icmp_echo(struct sk_buff *skb) 828 { 829 struct net *net; 830 831 net = dev_net(skb_dst(skb)->dev); 832 if (!net->ipv4.sysctl_icmp_echo_ignore_all) { 833 struct icmp_bxm icmp_param; 834 835 icmp_param.data.icmph = *icmp_hdr(skb); 836 icmp_param.data.icmph.type = ICMP_ECHOREPLY; (gdb) p net No symbol "net" in current context.
どうしても、netの値を見てみたいので、gccが最適化しないように少し細工する。
static変数にしてしまえば、gccも最適化しないだろう、と考えた。
% git diff diff --git a/net/ipv4/icmp.c b/net/ipv4/icmp.c old mode 100644 new mode 100755 index a0d847c..58b7fe9 --- a/net/ipv4/icmp.c +++ b/net/ipv4/icmp.c @@ -826,7 +826,7 @@ out_err: static void icmp_echo(struct sk_buff *skb) { - struct net *net; + static struct net *net; net = dev_net(skb_dst(skb)->dev); if (!net->ipv4.sysctl_icmp_echo_ignore_all) {
カーネルをビルドして、再度起動する。
もうそろそろ、毎回、UMLを起動するためにパラメータを入れるのが面倒になってきたので、シェルスクリプトに書いた。
% make ARCH=um % cat boot.sh #!/bin/sh sudo ~/linux-2.6/linux ubd0=uml-root-hardy.cow1,uml-root-hardy eth0=tuntap,tap0 umid=uml1 % ./boot.sh
% sudo gdb -p `ps aux | grep linux | awk '{ print $2 }' | head -1` [...] (gdb) b icmp_echo Breakpoint 1 at 0x819803e: file net/ipv4/icmp.c, line 832. (gdb) c Continuing.
% ping 192.168.10.1
icmp_echoでブレークした。
netの値を見ると、見られるようになった。
static変数にすることで、gccの最適化が効かなくなったのか、netシンボルが残るようになったようだ。
Breakpoint 1, icmp_echo (skb=0x9c7a980) at net/ipv4/icmp.c:832 832 if (!net->ipv4.sysctl_icmp_echo_ignore_all) { (gdb) l 827 static void icmp_echo(struct sk_buff *skb) 828 { 829 static struct net *net; 830 831 net = dev_net(skb_dst(skb)->dev); 832 if (!net->ipv4.sysctl_icmp_echo_ignore_all) { 833 struct icmp_bxm icmp_param; 834 835 icmp_param.data.icmph = *icmp_hdr(skb); 836 icmp_param.data.icmph.type = ICMP_ECHOREPLY; (gdb) p net $1 = (struct net *) 0x8242b10
gdbによる変数監視
さて、net->ipv4.sysctl_icmp_echo_ignore_allを監視する。
(gdb) watch net->ipv4.sysctl_icmp_echo_ignore_all Hardware watchpoint 2: net->ipv4.sysctl_icmp_echo_ignore_all
続行と思ったら、icmp_echoでまたブレーク。邪魔なので、ブレークポイントを削除する。
(gdb) c Continuing. Breakpoint 1, icmp_echo (skb=0x9c7a980) at net/ipv4/icmp.c:832 832 if (!net->ipv4.sysctl_icmp_echo_ignore_all) { (gdb) delete 1 (gdb) c Continuing.
UMLからicmp_echo_ignore_allの値を1にセットしてみる。
(uml)# echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
そうすると、gdbが変数の書き換えを検出。
0から1になったよ、と。
バックトレースを見ると、シェルで実行したechoによってコールされたwrite(2)から、VFS、procファイルシステムに書き込みが伝播して、最後にdo_proc_dointvec_conv関数まで伝わったことがわかる。
Hardware watchpoint 2: net->ipv4.sysctl_icmp_echo_ignore_all Old value = 0 New value = 1 0x08076807 in do_proc_dointvec_conv (negp=0x9644e53, lvalp=0x9644e44, valp=0x8242bfc, write=1, data=0x0) at kernel/sysctl.c:2231 2231 *valp = *negp ? -*lvalp : *lvalp; (gdb) bt #0 0x08076807 in do_proc_dointvec_conv (negp=0x9644e53, lvalp=0x9644e44, valp=0x8242bfc, write=1, data=0x0) at kernel/sysctl.c:2231 #1 0x08076ffb in __do_proc_dointvec (tbl_data=0x8242bfc, table=<value optimized out>, write=1, buffer=0x80fc408, lenp=0x9644ebc, ppos=0x9644f20, conv=0x80767e6 <do_proc_dointvec_conv>, data=0x0) at kernel/sysctl.c:2299 #2 0x08077187 in do_proc_dointvec (table=0x8234b04, write=157568595, buffer=<value optimized out>, lenp=0x9644ebc, ppos=0x9644f20, conv=0, data=0x0) at kernel/sysctl.c:2339 #3 0x08077238 in proc_dointvec (table=0x8234b04, write=1, buffer=0x80fc408, lenp=0x9644ebc, ppos=0x9644f20) at kernel/sysctl.c:2359 #4 0x080eaf35 in proc_sys_call_handler (filp=<value optimized out>, buf=0x80fc408, count=2, ppos=0x9644f20, write=1) at fs/proc/proc_sysctl.c:156 #5 0x080eaf67 in proc_sys_write (filp=0x9d54b40, buf=0x80fc408 "・r\f\017・E・\211\206・", count=2, ppos=0x9644f20) at fs/proc/proc_sysctl.c:174 #6 0x080b58f7 in vfs_write (file=0x9d54b40, buf=0x80fc408 "・r\f\017・E・\211\206・", count=2, pos=0x9644f20) at fs/read_write.c:366 #7 0x080b5ddc in sys_write (fd=1, buf=0x80fc408 "・r\f\017・E・\211\206・", count=2) at fs/read_write.c:418 #8 0x0805ab0c in handle_syscall (r=0x9ce7620) at arch/um/kernel/skas/syscall.c:35 #9 0x08068335 in userspace (regs=0x9ce7620) at arch/um/os-Linux/skas/process.c:201 #10 0x08058961 in fork_handler () at arch/um/kernel/process.c:181 #11 0x00000000 in ?? ()
同様に、/proc/sys/net/ipv4/icmp_echo_ignore_allを0にすると、変更が検出される。
1から0になった、と。
Hardware watchpoint 2: net->ipv4.sysctl_icmp_echo_ignore_all Old value = 1 New value = 0 0x08076807 in do_proc_dointvec_conv (negp=0x9644e53, lvalp=0x9644e44, valp=0x8242bfc, write=1, data=0x0) at kernel/sysctl.c:2231 2231 *valp = *negp ? -*lvalp : *lvalp;