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.

UMLに向けてpingを打つ。

% 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;

おわりに

かなり脱線したけど、/proc/sys/net/ipv4/icmp_echo_ignore_allへの書き込みの様子をgdbで解明できる所まできた。
段々面白くなってきた。