はじめに
本記事は、Microsoft Visual C/C++ が提供する CRT のセキュリティ機能 を使う場合の注意喚起を目的としています。
古典: strncpy の動作
C言語教科書 K&R 付録B3 文字列関数 より抜粋
char *strncpy(s,ct,n)
文字列 ct のうち最大 n 文字を s にコピーし, s を返す。
ct が n 文字より少ないときは,'\0' をつめる
古き文字列関数 strncpy
は、文字列バッファ s へ 必ず n 文字書き込みます。
ct の長さが n より短い場合は、残り n文字目まで '\0' を書き込みます。
ct の長さが n と等しいか長い場合は、n文字目までコピーするだけで、終端記号 '\0' を書きません。
JPCERTの推奨
上記の終端記号 '\0' を書き込まないという動作がバッファオーバーランの温床となるので、JPCERT のコーディング規約にて ISO-C11附属書K の strncpy_s
が適合コードとして示されています。
業務において JCERT のコーディング規約が採用されている場合は、これに従うことになるので strncpy_s
の動作仕様を見ていきましょう。
適合コード: strncpy_s の動作
errno_t strncpy_s(
char *strDest,
size_t numberOfElements,
const char *strSource,
size_t count
);
これらの関数は、strSource の最初の D 文字を strDest にコピーしようとします。このとき、D は count か strSourceの長さか、いずれか小さい方の値です。これらの D 文字が strDest 内に収まり (そのサイズが numberOfElementsとして指定されている) 終端のnullも収まる場合は、それらの文字はコピーされ、終端の null が追加されます。それ以外の場合は、 strDest[0] に null 文字が設定され、無効なパラメーターハンドラーが呼び出されます
コピー先に収まらない場合に例外ハンドラが呼び出される!
終端記号 '\0' を書き込めない場合、バッファオーバーランの温床とするぐらいならば例外で止める。という安全側へ全振りした仕様です。一応は抜け道が用意されており、
切り捨て動作が必要な場合は、次のように、_TRUNCATE を使用するか、size を 1 減算します。
strncpy_s(dst, 5, "a long string", _TRUNCATE);
strncpy_s(dst, 5, "a long string", 4);
上記の手段で回避は可能ですが、さらに次が大問題です。
strncpyとは異なり、countがstrSourceの長さを超える場合、変換先の文字列は、長さcountまで null 文字で埋め込まれません。
strncpy
はコピー先の文字列バッファの count 文字分に確実に書き込みますが、
strncpy_s
は、strcpy
と同様に文字列終端以後は不定値となります。
構造体の固定長文字配列へ strncpy
でコピーしていた処理を strncpy_s
へ置き換えた場合、確実に値が書き込まれずに不定値が生じ、再現困難なバグや、単体テストの障害になります(というか、なりました😡)
これらの関数のデバッグ ライブラリ バージョンでは、最初にバッファーを 0xFE で埋めます。 この動作を無効にするには、_CrtSetDebugFillThreshold を使用します。
なんと、デバッグ版ではさらに書き込み内容が変わります!
不定値ではなく固定値 0xFE で埋めるから嬉しい?いやいや埋めるなら 0x00 にしてくださいよ。
デバッグ版にて最初にバッファーを 0xFE で埋める動作は、 strncpy_s
に限らず、バッファーへ書き込む *****_s
関数の全てに共通する仕様のようです。
The debug versions of some security-enhanced CRT functions fill the buffer passed to them with a special character (0xFE).
Here's a list of the affected functions:
asctime_s, _wasctime_s
_cgets_s, _cgetws_s
ctime_s, _ctime32_s, _ctime64_s, _wctime_s, _wctime32_s, _wctime64_s
_ecvt_s
_fcvt_s
_gcvt_s
_itoa_s, _ltoa_s, _ultoa_s, _i64toa_s, _ui64toa_s, _itow_s, _ltow_s, _ultow_s, _i64tow_s, _ui64tow_s
_makepath_s, _wmakepath_s
_mbsnbcat_s, _mbsnbcat_s_l
_mbsnbcpy_s, _mbsnbcpy_s_l
_mbsnbset_s, _mbsnbset_s_l
_mktemp_s, _wmktemp_s
_splitpath_s, _wsplitpath_s
strcat_s, wcscat_s, _mbscat_s
strcpy_s, wcscpy_s, _mbscpy_s
_strdate_s, _wstrdate_s
strerror_s, _strerror_s, _wcserror_s, __wcserror_s
_strlwr_s, _strlwr_s_l, _mbslwr_s, _mbslwr_s_l, _wcslwr_s, _wcslwr_s_l
strncat_s, _strncat_s_l, wcsncat_s, _wcsncat_s_l, _mbsncat_s, _mbsncat_s_l
strncpy_s, _strncpy_s_l, wcsncpy_s, _wcsncpy_s_l, _mbsncpy_s, _mbsncpy_s_l
_strnset_s, _strnset_s_l, _wcsnset_s, _wcsnset_s_l, _mbsnset_s, _mbsnset_s_l
_strset_s, _strset_s_l, _wcsset_s, _wcsset_s_l, _mbsset_s, _mbsset_s_l
_strtime_s, _wstrtime_s
_strupr_s, _strupr_s_l, _mbsupr_s, _mbsupr_s_l, _wcsupr_s, _wcsupr_s_l
vsnprintf_s, _vsnprintf_s, _vsnprintf_s_l, _vsnwprintf_s, _vsnwpr`intf_s_l
まとめ
strncpy_s
は、古き文字列関数 strncpy
とは異なり、コピー先の文字列バッファのcount文字分を上書きせず、コピーした文字列終端以後は不定値となります。コピー元の文字列がコピー先の文字列バッファに収まらない場合は、例外ハンドラが呼び出されます。
Microsoft Visual C/C++ の固有実装なのか ISO-C11附属書K に明記されているのかわかりませんが、暗黙に上書き動作が変わってしまうのは困ります。皆さんも注意して strncpy_s
を使ってください。