第2回 バッファ不足の通知

前回は、strncpy に代わって使用できる my_strncpy を実装しました。
my_strncpy は、

  char *my_strncpy( char *s, const char *t, size_t n );

となっており、strncpy との唯一の違いはバッファの末尾を必ずヌル文字で終端して返す、という点です。

strncpy の場合は、バッファサイズが不足している場合には、バッファの終端にはヌル文字を付加しませんが、my_strncpy はバッファサイズが不足している場合でも、終端にはヌル文字が付加されることが保証されます。
この仕様により、呼び出し側でのバッファのヌル終端忘れによるセキュリティリスクを軽減させること、すなわち関数を呼び出す側に考慮したセキュリティの向上ができました。

ところが、これでもまだ関数の使い方によってはセキュリティ上の問題を発生させることがあります。

実際に my_strncpy を呼び出す典型的なコードは下記のようになるでしょう。

    char buf[ BUFSIZE ];

    my_strncpy( buf, s, sizeof( buf ) );
      :
    any_other_func( buf );    /* コピーされた文字列の再利用 */
      :

my_strncpy ではバッファはヌルで終端されていますので、buf に格納された文字列は安全に利用できそうに思えます。
ところが、当然のことですが、buf は s と等価であるとは限らないのです。
バッファのサイズが足りない場合には、my_strncpy は何も言わずに文字列を途中で切ってしまいます。
つまり、s が "/tmp/hasegawa_yosuke" を指している場合に、buf は "/tmp/hasegawa" を指しているかも知れません。

my_strncpy を呼び出す側は、強く意識しなければ、バッファが足りないことを検出することはできません。

    char buf[ BUFSIZE ];

    my_strncpy( buf, s, sizeof( buf ) );
    if( strlen( buf ) == sizeof( buf ) - 1 ){
        /* バッファが不足 */
    }

話の本筋から外れますが、ここで strlen の引数として外部からやってきた文字列 s を与えることは、可能な限り避けるべきです。s はヌル文字で終端していない可能性もあり、適切に例外処理等をしていない場合、あなたのプログラムは強制的に終了させられてしまいます。もちろん、相応の理由がある場合には s の中身を検査することもありますが、そうでない場合にはより安全側に動作することを意識したプログラミングを心がけましょう。

話を戻します。

繰り返しになりますが、my_strncpy を呼び出す側は、強く意識しなければ、バッファが足りないことを検出することができません。
これでは strncpy と変わりません。せっかくセキュアなコーディング − 自分の書いたコードを利用する側の安全 − にも気を配ってコーディングしているのですから、もう少し検討してみましょう。
my_strncpy 関数は、strncpy と同じくバッファのアドレスをそのまま返します。ところが、関数の呼び出し側でこの関数の戻り値を利用することは、おそらくほとんどないでしょう。なぜなら、バッファのアドレスは引数として与えているわけですから、呼び出し側にとってみると機知の情報だからです。ですので、関数の戻り値としてバッファの不足を通知してみるようにしましょう。

#define	FAIL		0
#define	SUCCESS		1

int my_strncpy2( char *s, const char *t, size_t n )
{
    char *s0 = s;
    
    if( n == 0 || s == NULL ) return FAIL;
    if( t == NULL ) t = "";
    n--;
    while( *t && n ){
        *s++ = *t++;
        n--;
    }
    *s = '\0';
    if( *t ) return FAIL; else return SUCCESS;
}

戻り値を int とし、文字列のコピーに成功した場合には SUCCESS を、バッファが不足している場合には FAIL を返すようにしました。
この関数のような、成功、失敗の2値を返す関数の戻り値について、成功、失敗をそれぞれゼロおよび非ゼロのいずれで表すかに大きな差はありませんが、プログラム全体を通じて混乱のないように方針を決めておきましょう。もちろん、int でなしに、_Bool や bool、BOOL を使用した場合も同じです。TRUE や FALSE が成功、失敗のいずれを表しているのかはプログラム全体で矛盾のないように決めておきましょう。

一般に、Win32API の場合、関数の型が BOOL である場合、呼び出しや処理に成功すれば TRUE (非ゼロ)を、失敗すれば FALSE (ゼロ)を返します。UNIXシステムコールでは、成功した場合にゼロを、失敗した場合に非ゼロ(-1)を返すものが多いようです。

さて、これで my_strcpy2 を利用する側では、関数の戻り値をみることにより、バッファが不足しているかどうかを判断できるようになりました。

    char buf[ BUFSIZE ];

    if( my_strcpy2( buf, s, sizeof( buf ) ) != SUCCESS ){
        /* バッファが不足 */
    }

ただし、呼び出し側で判断できるのは、バッファが不足しているかどうかだけであり、動的にバッファを確保しているような場合には、何バイトのバッファを確保すればよいのかは、やはり呼び出し側で計算してやる必要があります。

次回はこのあたりの話についてもう少し説明したいと思います。