yohhoyの日記

技術的メモをしていきたい日記

strncpy関数仕様のナゾ

C標準ライブラリが提供するstrncpy関数の仕様について。

有限長バッファへ文字列を切り詰めてコピーする処理では、strncpy関数が用いられるケースが多い。strncpy関数の仕様として「コピー元srcの文字列長が指定長BUFSIZE以上のとき、コピー先dest末尾にはNUL終端文字が設定されない」ため、下記★のようにコピー先バッファ末尾へのNUL終端文字代入が必須となる。この'\0'代入を忘れると正しいNUL終端文字列を構成しないケースが発生し、文字列としてdestを扱うタイミングでバッファオーバーランを引き起こす。

#define BUFSIZE 128
char dest[BUFSIZE];

void safe_store(const char* src)
{
  strncpy(dest, src, BUFSIZE);
  dest[BUFSIZE - 1] = '\0';  // ★
}

上記実装のためには不便な関数仕様に思えるが、これはstrncpy関数の目的が “長さ制限付きstrcpy関数” では無いことに起因する。strncpy関数は「構造体中にある固定長フィールドへの値設定*1」を意図したものであり、他の文字列処理関数のようにコピー先でNUL終端文字列を扱うことを想定していない。C標準ライブラリの策定時には、この既存設計がそのまま引き継がれた。

一方で、長さ制限付きstrcpy関数を意図したものとして下記候補が挙げられる。

  • FreeBSD由来*2で非標準の strlcpy関数。ただし、GNU Cライブラリ(glibc)では意図的*3strlcpy関数を提供しない。
  • C11標準ライブラリ Annex.K Bounds-checking interfaces拡張*4で定義されるstrncpy_s関数。ただしオプション扱いのため、全てのC11処理系で提供されるとは限らない。

C89(ANSI C)策定時の論理的根拠を説明する "Rationale for American National Standard for Information Systems - Programming Language - C" より引用。この説明文は(PDF)C99 Rationaleの§7.21.2.4にもそのまま引き継がれている。

4.11.2.4 The strncpy function
strncpy was initially introduced into the C library to deal with fixed-length name fields in structures such as directory entries. Such fields are not used in the same way as strings: the trailing null is unnecessary for a maximum-length field, and setting trailing bytes for shorter names to null assures efficient field-wise comparisons. strncpy is not by origin a "bounded strcpy," and the Committee has preferred to recognize existing practice rather than alter the function to better suit it to such use.

C99 7.21.2.4/p2-3より引用。

2 The strncpy function copies not more than n characters (characters that follow a null character are not copied) from the array pointed to by s2 to the array pointed to by s1.269) If copying takes place between objects that overlap, the behavior is undefined.
3 If the array pointed to by s2 is a string that is shorter than n characters, null characters are appended to the copy in the array pointed to by s1, until n characters in all have been written.

脚注269) Thus, if there is no null character in the first n characters of the array pointed to by s2, the result will not be null-terminated.

関連URL

*1:本文中コードのように strncpy 関数を利用する際には気付きにくいが、strncpy 関数仕様では「コピー元文字列が指定長よりも短い場合、コピー先バッファの末尾側を全てNUL終端文字で埋める」となっている。これはNUL終端文字列として扱う際には冗長な処理だが、固定長フィールドを単純バイト比較するためには必須な処理となる。

*2:https://www.sudo.ws/todd/papers/strlcpy.html

*3:strlcpy 関数によりバッファオーバーラン脆弱性は防げるが、文字列切り詰めによる意図しないデータ欠落もまたセキュリティ上の懸念となるという理由。詳細はThe ups and downs of strlcpy()参照。

*4:標準ヘッダ string.h の include 前にマクロ __STDC_WANT_LIB_EXT1__ を1で定義すると、strncpy_s 関数が利用可能となる。