NAKAMURA Minoru の日記 (2008年8月)

先月の日記(2008年07月) 今月の日記(2008年08月)
2002 | 10 | 11 | 12
2003 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2004 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2005 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2006 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2007 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2008 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2009 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2010 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2011 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2012 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2013 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2014 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2015 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2016 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2017 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11
ホームページ | 最新のコメント50
インデックス: 食べ歩き | Java | プログラム | UNIX | 画像
最新の日記へのリンク | この日記ページをはてなアンテナに追加 この日記ページをはてなブックマークに追加
はてな ダイアリー アンテナ ブックマーク ブログ
Twitter | mixi | Facebook | Google+
slideshare | github | Qiita



8/15 (金)

[Linux] RLIMIT_SIGPENDING の奇妙な動作に嵌まる

Linux カーネル 2.6.9 をベースにした RedHat Enterpirse Linux 4 上で某システム開発をしているのだが、最近怪しい SIGSEGV シグナルが送信されるというバグに遭遇して2.5日を費やしてしまった。

謎のヌルポインタ SIGSEGV シグナル

発端は開発中のシステムのデバッグ中に、si_code が 0、si_addr が NULL を指す SIGSEGV シグナルが記録されたこと。

  • アクセスしているメモリ番地は 0 じゃないのに si_addr はなぜか 0 になっている。
  • si_code が 0 もなっている。 通常のメモリアクセスでは SEGV_MAPERR(1)、SEGV_ACCERR(2) になるはずで、SIGSEGV シグナルの si_code が 0 になるのは kill で SIGSEGV シグナルを送りつけた時だけ。 当然、そんなことはしていない。

某システムは、動的バイナリ変換(JIT コンパイラ)を載せているため障害の再現が難しい上に、最近はいろいろ境界的な障害を出し続けていてあれもこれも疑わしく思えるのが辛い(7月29日8月3日8月11日)

Linux カーネルのあちこちに printk を仕込んでごにょごにょ見ていると、結局、「SIGSEGV シグナルが発生したことが sigpending リストに記録されたが、siginfo がキューに載せられなかった」というのが原因らしい。 この場合、Linux カーネルは collect_signal 関数の中でゼロクリアされた siginfo をでっち上げて返すので、ヌルポインタアクセスしたような SIGSEGV シグナルが送信されていたようだ。

kernel/signal.c:
static inline int collect_signal(int sig, struct sigpending *list, siginfo_t *info)
{
	struct sigqueue *q, *first = NULL;
	int still_pending = 0;

	if (unlikely(!sigismember(&list->signal, sig)))
		return 0;

	/*
	 * Collect the siginfo appropriate to this signal.  Check if
	 * there is another siginfo for the same signal.
	*/
	list_for_each_entry(q, &list->list, list) {
		if (q->info.si_signo == sig) {
			if (first) {
				still_pending = 1;
				break;
			}
			first = q;
		}
	}
	if (first) {
		list_del_init(&first->list);
		copy_siginfo(info, &first->info);
		__sigqueue_free(first);
		if (!still_pending)
			sigdelset(&list->signal, sig);
	} else {

		/* Ok, it wasn't in the queue.  This must be
		   a fast-pathed signal or we must have been
		   out of queue space.  So zero out the info.
		 */
		sigdelset(&list->signal, sig);
		info->si_signo = sig;
		info->si_errno = 0;
		info->si_code = 0;
		info->si_pid = 0;
		info->si_uid = 0;
	}

	return 1;
}

RLIMIT_SIGPENDING の奇妙な制限

問題はなぜキューが溢れてしまったかという点。 どうもリアルタイムシグナルの待ち受けキュー長を制御している RLIMIT_SIGPENDING が、非リアルタイムシグナルにも適用されるのが原因らしい。

テストコードをいろいろ書いてみると RHEL4 のベースとなっている Linux カーネルは、RLIMIT_SIGPENDING が受信するシグナル数を制限するだけではなく、送信するシグナル数にも影響しているようだ。 しかもリアルタイムシグナルではなく通常の(非リアルタイム)シグナルの方を制約している。

結果として、一定数以上のシグナルを送信した後にメモリ保護違反を引き起こすようなメモリアクセスを行うと、それは全部ヌルポインタへアクセスした SIGSEGV シグナルとして報告される。 泣きたくなるような変な動作だ。

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sched.h>
#include <sys/prctl.h>
#include <sys/resource.h>

// シグナルハンドラ (si_addr を表示するのが目的)
void signal_handler(int sig, siginfo_t* info, void* data) {
    printf("singal: signo=%d si_addr=%p\n", sig, info->si_addr);
    exit(0);
}

// RLIMIT_SIGPENDING の最大値を変更
void set_max_signals(int max) {
    struct rlimit limit;
    getrlimit(RLIMIT_SIGPENDING, &limit);
    limit.rlim_cur = max;
    setrlimit(RLIMIT_SIGPENDING, &limit);
}

// 親プロセスは子プロセスにシグナルを送信するが、
void do_parent() {
    int i;
    
    // 受信可能なリアルタイムシグナル数を制限
    set_max_signals(maxqueue);

    // 子プロセスの fork 完了を待つ
    sleep(1);

    // 子プロセスに自身が受信可能なリアルタイムシグナル数分だけ
    // シグナルを送信する(子プロセスが受信可能な数ではない)
    // 送信するシグナル数を調整すると、SIGSEGV シグナルが正常にでるようになる
    for (i=0 ; i<maxqueue; i++) {
        sigval_t dummy;
        ret = sigqueue(child, sigrt, dummy);

        // sigqueue がエラーとなったら SIGSEGV シグナルをわざと起こすメモリアクセスへ移動
        if (ret < 0) {
            fprintf(stderr, "ret=%d\n", ret);
            perror("sigqueue");
            goto label;
        }
    }

    // SIGSEGV シグナルを強制的に起こす。
    // si_addr は (unsigned long)-1 にならないとおかしい
  label:
    *(char*)(-1) = 0;
}

// 子プロセスは何もしない
void do_child() {
    // ゾンビーにならないように親プロセスが死んだら消える
    if (prctl(PR_SET_PDEATHSIG, SIGKILL)) {
      perror("prctl"), exit(2);
    }

    // 何もしない
    for (;;) {
      sched_yield();
    }
}

int main(int argc, char** argv) {
    int ret, maxqueue = 32, sigrt = SIGRTMIN + 3;

    // sigrt 番のリアルタイムシグナルをブロック
    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, sigrt);
    sigprocmask(SIG_BLOCK, &sigset, NULL);

    // SIGSEGV シグナルハンドラを設定
    struct sigaction act;
    act.sa_flags = SA_SIGINFO;
    sigfillset(&act.sa_mask);
    act.sa_sigaction = signal_handler;
    sigaction(SIGSEGV, &act, NULL);

    pid_t child = fork();
    if (child > 0) {
        do_parent();
    } else {
        do_child();
    }

    return 0;
}

最近の Linux カーネルでは直っているのだが(2.6.16 で確認)、RHEL5 は大丈夫かしらん?


8/12 (水)

すごい勢いでスパムメールが来ているんですけど

お盆には先祖の霊が帰ってくるというが、本日は送った覚えのないスパムメールが4千通以上帰って来た。

エラーメールの From 部分は大学の研究室(MTL.T)のメールアドレスになっている。 このメールアドレスを語って適当なメールサーバにスパムを送信し、エラーメールが研究室のメールサーバに帰ってくるのを期待しているらしい。

MTL.T のメールアドレスはそのままゴミ箱に行きなのだが、この流量でスパム来ると /var が溢れてしまう可能性があるのが問題だ。 なんとかせねばならない。

追記:8/16

だいぶ下火になったがここ数日で3万通を越えるスパムエラーメールを受け取った。


8/11 (月)

[CPU] IA-64 のデバッグフォルトにまた祟られる

またデバッグレジスタ周辺でバグが出た…。
IA-64 のTLB系割り込みを極めたと信じていたのだが、いろいろ思い違いをしていたことに気づかされる。

x86 のデータブレークポイントは検査対象のロード・ストア命令が実行された「後」に割り込みが入るトラップ型なのだが、IA-64 の場合実行される「前」に割り込みが入るフォルト型のシステムだ。 カーネルをいろいろ弄る分には、実行される前に割り込みが入るデバッグレジスタ機能の方がいろいろ応用が利く。 検査対象ストア命令が書き込む前と後を手に入れることができるし、7月29日に書いたようにデバッグレジスタをデバッグ以外のことに利用できたりする。

ただ実行前に割り込みを揚げるフォルト型の場合、割り込みからそのままリターンすると再度同じ命令が実行されるため、PSR に dd ビットや da ビットが存在する。 このビットを立てて割り込みから再開すると、1命令分だけデータブレーク機能が抑止されることになる(2007年8月10日の日記)

ところで IA-64 は不正境界アクセスが一部の例外を除いてできず、「不正境界参照フォルト」と呼ばれるフォルトを発生する。 IA-64/Linux カーネルはこのフォルトが起きた時にカーネルの内部で不正境界参照フォルトを起こしたメモリアクセス命令をエミュレーション実行してしまう。 その後で命令アドレスを1命令分進めて実行する

問題はメモリアクセスが不正境界でありながら同時にデバッグブレーク検査に引っかかった場合だ。

  1. 割り込みの優先順位としてデータデバッグフォルト > 不正境界参照フォルトなので、先に「データデバッグフォルト」が発生する。 独自フォルトハンドラの中で PSR.dd=1 に設定する。
  2. 割り込みから復帰した後は PSR.dd=1 の効果によるデータブレーク検査が回避され、不正境界参照フォルトが起きる。 この時、PSR.ddはまだ立ったままみたい
  3. 不正境界参照フォルトハンドラの中でメモリアクセスをエミュレーションして命令カウンタを一つ進めて割り込みから復帰する。

PSR.dd=1 の効果が最初の不正境界なメモリアクセスではなく、その次の命令に転移してしまうことに。 もちろんプログラムはうまく動作しなくて…


8/3 (日)

[CPU] 命令コードのフラッシュを忘れて IA-64 が誤動作

IA-64 上で動作する動的バイナリ変換というか JIT コンパイラを弄っているのだが、 コードパッチ後に命令キャッシュをフラッシュするのを忘れて誤動作するというバグに嵌まる。

IA-64 は命令キャッシュとデータキャッシュの間で一貫性がとられないので、命令を書き換えた後はキャッシュフラッシュ命令(fc.i)を実行しないとダメだということは当然知っていたのだが、ソース中にうっかり書き漏らしていた。 結局、このバグ対策を金曜日からはじめて 15 時間ぐらい時間の無駄遣いをしているよ。 orz

ところで msync 関数に MS_INVALIDATE を指定した場合、IA-64 アーキテクチャではキャッシュフラッシュの効果があると信じていたが、IA-64/Linux で実験すると fc.i が出ていないようにみえる。 POSIX の msync の解説には以下のような文言がある。 同一プロセッサのデータキャッシュと命令キャッシュが異なる場合まで別々にカウントして all cached copies なのかどうかは謎だけどね…

When MS_INVALIDATE is specified, msync() shall invalidate all cached copies of mapped data that are inconsistent with the permanent storage locations such that subsequent references shall obtain data that was consistent with the permanent storage locations sometime between the call to msync() and the first subsequent memory reference to the data.

先月の日記(2008年07月) 今月の日記(2008年08月)
2002 | 10 | 11 | 12
2003 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2004 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2005 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2006 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2007 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2008 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2009 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2010 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2011 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2012 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2013 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2014 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2015 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2016 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
2017 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11
ホームページ | 最新のコメント50
インデックス: 食べ歩き | Java | プログラム | UNIX | 画像
最新の日記へのリンク | この日記ページをはてなアンテナに追加 この日記ページをはてなブックマークに追加
はてな ダイアリー アンテナ ブックマーク ブログ
Twitter | mixi | Facebook | Google+
slideshare | github | Qiita


Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp