strncpy_s で嵌った話: 文字列終端からバッファ末尾までの領域の扱いが strncpy と違う！

## はじめに

本記事は、Microsoft Visual C/C++ が提供する [CRT のセキュリティ機能](https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/security-features-in-the-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` が適合コードとして示されています。

https://www.jpcert.or.jp/sc-rules/c-api07-c.html

業務において JCERT のコーディング規約が採用されている場合は、これに従うことになるので `strncpy_s` の動作仕様を見ていきましょう。

## 適合コード: strncpy_s の動作

https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/strncpy-s-strncpy-s-l-wcsncpy-s-wcsncpy-s-l-mbsncpy-s-mbsncpy-s-l
```CPP
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 減算します。
```CPP
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` 関数の全てに共通する仕様のようです。

https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/crtsetdebugfillthreshold
> 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` を使ってください。

## END