😎

C++ の std::strncpy 関数の危険な (?) 仕様

2023/04/08に公開

はじめに

C++ の標準ライブラリに std::strncpy 関数がありますね。
ヌル終端バイト文字列[1]をコピーするものです。

よく似た名前の std::strcpy 関数との違いはコピーする文字数を指定できることです。
ヌル終端文字列の扱いで注意しないとならないことのひとつはバッファオーバーランですが、同じ <cstring> ヘッダで提供される文字列操作関数は 'n' つきのものを使っておけば安心とお考えの方もいらっしゃるのではないでしょうか。

ところが、 std::strncpy 関数はそうではないのです... という話です。

仕様

まずは同関数の仕様を見てみましょう。

std::strncpy 関数 (C++)

C++20 規格 (のドラフトである N4861[2]) によりますと:

namespace std {
  char* strncpy(char* s1, const char* s2, size_t n);
}

The contents and meaning of the header <cstring> are the same as the C standard library header <string.h>.

(21.5.3 Header <cstring> synopsis [cstring.syn])

C++ の規格では、 C 由来のライブラリは C の規格を参照するようになっています。

strncpy 関数 (C)

C++20 が参照する C 規格は C17 ですが、手元にあった C23 のドラフト (N3054[3]) でご容赦願います。

char *strncpy(char * restrict s1, const char * restrict s2, size_t n);

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. If copying takes place between objects that overlap, the behavior is undefined.

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.

また、ひとつめのパラグラフのひとつめの文 (The strncpy function copies ... pointed to by s1.) には次の注釈があります:

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

(7.26.2.5 The strncpy function)

仕様まとめ

つまり、 std::strncpy(char* s1, const char* s2, size_t n) 関数は、

  1. s2 が指す配列から s1 が指す配列に文字をコピーする。
  2. 最大 n 文字をコピーする。 (n 文字を超えてコピーすることはない)
  3. ヌル文字より後ろの文字はコピーしない。
  4. コピー元とコピー先がオーバーラップしている場合の動作は未定義。
  5. s2 が指す文字列の長さが n 文字よりも短い場合、 n 文字に達するまで s1 が指す配列にはヌル文字が書き込まれる。
  6. s2 が指す配列の先頭から n 文字までにヌル文字が現れない場合、コピー結果はヌル終端されない。

このうち、 (1), (2), (3), および (4) は違和感のないものでしょう。

ふしぎな仕様

(5) は少々驚かれるかもしれません。

  1. s2 が指す文字列の長さが n 文字よりも短い場合、 n 文字に達するまで s1 が指す配列にはヌル文字が書き込まれる。

仕様 (5) の例
figure 1. 仕様 (5) の例

ただ、この動作によって特に困ることはないですね。

危険な仕様

問題なのは (6) です。

  1. s2 が指す配列の先頭から n 文字までにヌル文字が現れない場合、コピー結果はヌル終端されない。

仕様 (6) の例
figure 2. 仕様 (6) の例

figure 2 の例のようにコピー後のバッファはヌル終端されないことがあり得るので、それをヌル終端文字列として扱ってしまうとバッファオーバーランにつながります。
'n' つきの std::strncpy 関数を使っておけば安心と油断していると、ひどい裏切りにあったと思うことでしょう。
やっかいなのは、 figure 1 の例のように場合によってはヌル終端されることもあるので、バグを埋め込んでしまってもそれに気づきにくいことだと思います。

仕様の背景

なぜ、一見不可解とも思えるこのような仕様になっているのかは、 yohhoy さんの次の記事をご覧いただくとご理解・ご納得できると思います:

https://yohhoy.hatenadiary.jp/entry/20161102/p1

対策

仕様 (6) によりコピー後のバッファがヌル終端されないことがある事象の対策を考えてみます。
あいにく、あらゆる状況で適用可能なユニバーサルな方法はないと思います。

i) コピー先のバッファの長さがあらかじめ決まっている場合

まず、コピー元の文字列の長さによらずコピー先のバッファの長さが決まっている場合です。
つまり、バッファの長さが足りない場合は途中までコピーすればよいということですね。

この場合は std::strncpy 関数に確実にヌル終端してもらうことはできないので、プログラマー自身がヌル終端する必要があります。

example 1
void example1(const char *src) {
  constexpr std::size_t bufferLength{...};
  char buffer[bufferLength];
  std::strncpy(buffer, src, bufferLength);  // コピーしたあとで、
  buffer[bufferLength - 1] = '\0';          // 確実にヌル終端する。
  ...
}

コピー元の文字列の長さがコピー先のバッファの長さよりも短い場合は std::strncpy 関数がヌル終端してくれますので、プログラマーがヌル文字を代入する場所は常にバッファの最後の要素でかまいません。

ii) コピー先のバッファの長さを実行時に決められる場合

逆に、コピー元の文字列の長さに合わせてコピー先のバッファの長さを決められる場合です。
コピー元の文字列を欠けることなくすべてコピーしたいということですね。

この場合は十分な長さのバッファを確保することで、 std::strncpy 関数に確実にヌル終端してもらうことができます。

example 2a
void example2a(const char *src) {
  const std::size_t bufferLength{std::strlen(src) + 1}; // ヌル終端文字用の「+ 1」
  auto buffer = std::make_unique<char[]>(bufferLength);
  std::strncpy(buffer.get(), src, bufferLength);
  ...
}

std::strncpy 関数を使っているようなコードにスマートポインタは登場しないかもしれないので...

example 2b
void example2b(const char *src) {
  const std::size_t bufferLength{std::strlen(src) + 1}; // ヌル終端文字用の「+ 1」
  char *buffer{new char[bufferLength]};
  std::strncpy(buffer, src, bufferLength);
  ...
  delete[] buffer;
}

おわりに

std::strncpy 関数は手軽に使えてしまう割りには仕様が直感的ではなく「正しく」使うのも難しいと思います。
figure 2 のような危険なコードをときどき見かけますし、つい先日も見かけたので本記事を書きました。
コードレビューで見つけるのも難しいですしね。

NTBS や <cstring> の使用は避け、文字列の取り扱いはすべて std::string, std::string_view クラスとするようにしたいです。

脚注
  1. 規格では null-terminated byte string または NTBS と呼ばれます。 (16.4.2.2.5.1 Byte strings [byte.strings]) ↩︎

  2. Working Draft, Standard for Programming Language C++ [N4861]. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4861.pdf. ↩︎

  3. Working Draft, Standard for Programming Language C [N3054]. https://open-std.org/JTC1/SC22/WG14/www/docs/n3054.pdf. ↩︎

Discussion