kakts-log

programming について調べたことを整理していきます

C: シグナルハンドラ内で非リエントラントな関数を扱った際の挙動

概要

Cにおいて、シグナルハンドラ関数内で非リエントラントな関数を実行した際に意図しない挙動が生じる問題についてかんたんに整理します。

リエントラント: Reentrant 再入可能 - マルチスレッド安全なもの
非リエントラント: Non-Reentrant 再入不可 - リエントラントとは逆で、複数のスレッドから同時に実行した場合、予期せぬ結果を引き起こす可能性があるもの

シグナルハンドラで不整合が起きる例

シグナルハンドラ関数内で非リエントラントな関数であるcrypt(3)を実行した例を挙げ、この場合の注意点について整理します。

crypt(3)について

Cの標準ライブラリで用意されているcrypt(3)は、文字列の暗号化を行う関数で、非リエントラントな関数となります。

crypt(3)実行時、関数内部でstaticな変数を操作するため、複数のスレッドで同時に実行された際に、そのstaticな変数を同時に操作されることにより、結果として意図せぬ結果となります。

crypt_r(3)は、crypt(3)のリエントラント対応版の関数となります。

Man page of CRYPT

シグナルハンドラでcrpyt(3)を使った例

ここでシグナルハンドラ関数内とmain関数内でcrypt(3)を使用した際に生じる不具合を確認します。

/**
 * @file nonreentrant.c
 * 21-1 main()とシグナルハンドラの双方でnon-reentrant関数を呼び出す
 */

// 600以上の値で定義するとSUSv3関連の定義とC99での定義が追加で公開される
#if ! defined(_XOPEN_SOURCE) || _XOPEN_SOURCE < 600
#define _XOPEN_SOURCE 600
#endif

#include <unistd.h>
#include <signal.h>
#include <string.h>
#include "../lib/tlpi_hdr.h"

// argv[2]の文字列
static char *str2;

// ハンドラ実行回数
static int handled = 0;

/**
 * シグナルハンドラ
 * str2 をcrypt()で暗号化する
 */
static void handler(int sig)
{
    crypt(str2, "xx");
    handled++;
}

int main(int argc, char *argv[])
{
    char *cr1;

    int callNum, mismatch;
    struct sigaction sa;

    // 引数チェック
    if (argc != 3) {
        usageErr("%s str1 str2\n", argv[0]);
    }

    // argv[2]をstatic変数に代入し、ハンドラから使用可能にする
    str2 = argv[2];
    
    // staticに割り当てられた文字列を別メモリへコピー
    cr1 = strdup(crypt(argv[1], "xx"));

    if (cr1 == NULL) {
        errExit("strdup");
    }

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handler;
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        errExit("sigaction");
    }

    // argv[1]を繰り返しcrypt()で暗号化する。
    // シグナルハンドラが割り込むと、crypt()内部のstatic memoryがargv[2]の暗号化結果に上書きされ、
    // strcmp()がcr1との不一致を検出する

    for (callNum = 1, mismatch = 0; ; callNum++) {
        
        // すでにargv[1]を暗号化した文字列cr1 と 再度argv[1]を暗号化した文字列を比較する
        if (strcmp(crypt(argv[1], "xx"), cr1) != 0) {
            mismatch++;
            printf("Mismatch on call %d (mismatch=%d handled=%d)\n", callNum, mismatch, handled);
        }
    }
}

ここでは主に以下の処理を行っています。

  1. 実行時に文字列を2つ受け取る。str1, str2とする
  2. main関数内でstr1に対してcrypt(3)を実行し、変数に代入する
  3. SIGINTに対するシグナルハンドラ関数内で、str2に対してcrypt(3)を実行する
  4. main関数内のループで、2とは別にstr1に対してcrypt(3)を実行した結果と、2で変数に代入した文字列を比較する。
  5. プログラム実行中に、SIGINTを送信し、4の処理で不整合が起きるかをチェックする

SIGINTを受け取った際、シグナルハンドラは別スレッドで実行された際に、crypt(3)が実行されます。
このときcrypt(3)内で扱っているstaticな変数に対する競合が発生し、4で実行しているcrypt(3)が意図しない結果となり、不整合が起きることを確認するものです。 (Linuxプログラミングインタフェース 21-1参照)

コード実行後、SIGINTを投げるたびに文字列が一致せず、ログに出ていることが確認できます。

root@8db89fb5ba52:/tlpi/signals# ./nonreentrant a b
^CMismatch on call 352950 (mismatch=1 handled=1)
^CMismatch on call 1839887 (mismatch=2 handled=2)

非同期シグナルセーフな関数

シグナルハンドラを扱うにあたって上述した問題が発生しないことを保証するために、Linuxの標準仕様において、非同期シグナルセーフな関数が指定されています。

docs.oracle.com

www.jpcert.or.jp

シグナルハンドラ関数を扱う際の注意点まとめ

上記のような問題が発生するため、シグナルハンドラ関数では、以下を満たすように注意が必要です

  • シグナルハンドラ関数内ではリエントラントであるようにする、もしくは上述した非同期シグナルセーフな関数のみを使用する。
  • メインプログラムが下記の処理を行う際は、シグナルをブロックする
    • 非同期シグナルセーフでない関数の実行
    • シグナルハンドラも変更するグローバルデータの変更