- cURLプロジェクトは、従来の
strncpy()の削除に続き、今度は**strcpy()もコードベースで完全に禁止**した
strcpy()はAPIが単純だが、バッファサイズの検証が分離される危険があり、長期的なメンテナンスでは安全ではない
- その代わりに**
curlx_strcopy()**という新しい関数が導入され、宛先バッファサイズと文字列長の両方を引数として受け取り、コピー可能かどうかを検査してから実行する
- この関数は内部的に
memcpy()を使用し、ヌル終端文字の処理まで保証する
- こうした変更により、セキュリティとコードの一貫性を高め、AIが誤った脆弱性レポートを生成する問題も減らせる
strcpy削除の背景
- cURLは過去に
strncpy()呼び出しをすべて削除しており、この関数の直感的でないAPI、ヌル終端を保証しない点、不要な0埋めの問題を指摘していた
- 部分文字列のコピーが必要な場合は
memcpy()を使用し、ヌル終端を手動で処理するよう変更
strcpy()はAPIが単純だが、バッファサイズを明示しないため、メンテナンス中に検証コードとコピー呼び出しが分離される危険がある
- コードが数十年にわたって複数の開発者によって修正される場合、バッファサイズ検証が無効化される可能性がある
新しい文字列コピー関数の導入
- こうした危険を防ぐため、**
curlx_strcopy()**という代替関数を導入
- 引数として宛先バッファ、バッファサイズ、元バッファ、元文字列長を受け取る
- コピーとヌル終端の両方が可能な場合にのみ
memcpy()で実行
- 失敗時は宛先バッファを空文字列に初期化
- この関数は
strcpy()より引数もコード量も多いが、バッファ検証をコピー処理と密接に結び付けることで安全性を確保する
- cURLコードベースでは
strcpy()の使用を完全に禁止し、strncpy()と同様に削除した
実装の詳細
- 関数定義の例は次のとおり
void curlx_strcopy(char *dest, size_t dsize, const char *src, size_t slen)
{
DEBUGASSERT(slen < dsize);
if(slen < dsize) {
memcpy(dest, src, slen);
dest[slen] = 0;
}
else if(dsize)
dest[0] = 0;
}
DEBUGASSERTにより開発中のエラーを早期検知し、実際の配布環境では常に成功するよう設計されている
strcpyのような戻り値はなく、テストおよびファジング段階でエラーを捕捉する方式を採用している
コミュニティの反応
- 一部の開発者は**
strcpy_s()(C11 Annex K)に似ていると指摘したが、cURLは依然としてC89標準**を使用している
- 別の意見としては、戻り値追加の必要性やバッファ失敗時の処理改善の提案があった
- これに対しcURL側は、「常に成功する関数として設計されているため戻り値は不要だ」と説明した
AI関連の副次効果
- 今回の変更により、AIチャットボットがcURLコード内のstrcpy使用を誤って検出し、『脆弱だ』と主張する問題を防げる
- ただし筆者は、「AIが別の虚偽レポートを作り出す可能性は依然としてある」として、AIベースのコード分析の限界にも言及している
5件のコメント
strcpyの代わりにsnprintfを使うのが正しい。コードにstrcpyがあるなら、それを作った開発者の所在地を突き止めるべきだ。これは25年前にゲーム会社に勤めていた頃、デバッグコードとしてやっていたやり方でしたが、
strcpyだけの話ではないでしょう。リリース版では速度向上のために再び制限を外した状態でサービスされていました。実際、ゲーム業界はメモリ衝突に最も敏感なので、作業も非常に慎重に気を引き締めて進めていましたし、メモリデバッガも独自に作って使っていました。ところが今日になって振り返ると、それはガベージコレクションを作っていたようなものだったのです。ほのかな思い出ですね。エラー C4996
'strcpy': この関数または変数は安全でない可能性があります。代わりにstrcpy_sの使用を検討してください。非推奨警告を無効にするには、_CRT_SECURE_NO_WARNINGSを使用します。詳細はオンラインヘルプを参照してください。Hacker Newsの意見
strcpy() はセキュリティ面だけでなく、性能面でも良くない
以前は文字列長が分からないときは strcpy() が効率的だと思っていたが、実際には 1 バイトずつコピーする構造なので CPU が分岐予測を行う必要があり、これは非効率的である
C の文字列ルーチンはどれもこれも 大きな制約があって役に立たないと感じていた
そのため、文字列ポインタと一緒に 割り当てられたメモリサイズを記録するライブラリがぜひ必要だと思う
例として bstring ライブラリ は参考になる
char username[20]のようなフィールドに NUL を詰めて埋める用途だった。関連文書は string_copying.7 マニュアル を参照curlx_strcopy が成功可否を返さないのは不可解だ
dest[0] を検査することもできるが、これは エラーを誘発しやすく直感的でもない
DEBUGASSERT(slen < dsize);が通れば成功と見なしているのだろうが、release ビルドでは assert が除去される可能性がある。明示的なエラーコードのほうがよいと思うstrncpy() はもともと null-terminated 文字列のためのものではなく、固定長フィールドのための関数だった
問題は静的解析器が strcpy の代わりに strncpy を使うよう推奨したことから始まった。実際の代替は snprintf や strlcpy だった
この API はまるで Annex-K のように感じる。宛先バッファサイズには NUL 用の領域が含まれるが、ソースサイズには含まれない
むしろ memcpy を直接使うほうがましだと思う
記事中の「strcpy は AI が誤った脆弱性レポートを作り出すための餌」という言葉が印象的だった
「コードの近くで検査せよ」という原則は良いが、データの ライフサイクルの初期で検査すべき場合は曖昧になる
Rust の Result 型のように、「検証済みデータ」であることを型で区別できればよいと思う
バッファサイズと文字列長の off-by-one の差はひどいユーザビリティ問題だ。今後もエラーを誘発する可能性が高い
新たに提案された文字列コピーファンクションは、コピーできない場合に 対象バッファを空にして void を返す
しかしこのような場合は エラーとして扱い、バッファには手を触れないほうがよいと思う。DEBUGASSERT だけで防ぐのは不安だ
プロジェクト完成おめでとう。C/C++ でも努力すれば メモリ安全性を確保できる
ただしモバイル環境では グラフのフォントサイズが小さすぎて読みづらい
C3言語へ完全に移行するのも良いですね。C言語の構文を最小限の変更にとどめつつ、モダンな機能を追加したプロジェクトなので、移行もしやすいです。