紳士プログラマーは、 memset関数を使用して、C ++プログラムで何かをする必要があります! むしろ、何をすべきかすぐに明らかです-それを使用して停止する必要があります。 当時、「 C / C ++の世界で最も危険な関数 」という記事を書きました。 この記事はmemsetに関するものだと推測するのは簡単だと思います。
ただし、私は根拠がないわけではなく、例を使用してこの機能の危険性を再度示します。 Chromiumプロジェクトとそれに使用されるライブラリのコードは非常に高品質です。 Google開発者は、テストと、欠陥を検出するためのさまざまなツールの使用に多くの注意を払っています。 たとえば、GoogleはAddressSanitizer、ThreadSanitizer、MemorySanitizerなどのツールを開発しました。
その結果、 memset関数に関連するエラーはほとんどありませんが、それらがまだ存在するのは残念です。 非常に高品質のプロジェクトですが、それでもそうです!
PVS-Studioによって発行されたレポートの解析中に私が気付いたことを見てみましょう。 入門記事で書いたように、レポートを非常に流fluentに見たので、気づいていない他のエラーがあるかもしれません。 それでも、見つかった欠陥は、 malloc関数について説明するのに十分です。
誤って計算されたバッファサイズ
最初のタイプのエラーは、バッファサイズの誤った計算に関連しています。 または、言い換えると、問題は、バイト単位の配列のサイズと配列内の要素の数との間に混乱があるということです。 このようなエラーは、 CWE-682 :Incorrect Calculationに分類できます。
最初のエラー例は、Chromiumプロジェクトコードから直接取得されています。 textおよびunmodified_text配列はUnicode文字で構成されていることに注意してください。
#if defined(WIN32) typedef wchar_t WebUChar; #else typedef unsigned short WebUChar; #endif static const size_t kTextLengthCap = 4; class WebKeyboardEvent : public WebInputEvent { .... WebUChar text[kTextLengthCap]; WebUChar unmodified_text[kTextLengthCap]; .... };
その結果、これらの配列の要素の半分のみがゼロで埋められます。
WebKeyboardEvent* BuildCharEvent(const InputEventData& event) { WebKeyboardEvent* key_event = new WebKeyboardEvent(....); .... memset(key_event->text, 0, text_length_cap); memset(key_event->unmodified_text, 0, text_length_cap); .... }
PVS-Studioの警告:
- V512 CWE-682 'memset'関数の呼び出しは、バッファー 'key_event-> text'のアンダーフローを引き起こします。 event_conversion.cc 435
- V512 CWE-682 'memset'関数の呼び出しは、バッファー 'key_event-> unmodified_text'のアンダーフローを引き起こします。 event_conversion.cc 436
エラーの2番目の例は、Chromiumで使用されるWebRTCライブラリから取得されます。 このエラーは前のエラーと似ています。配列の要素がint64_t型であることは考慮されていません。
class VCMRttFilter { .... enum { kMaxDriftJumpCount = 5 }; .... int64_t _jumpBuf[kMaxDriftJumpCount]; int64_t _driftBuf[kMaxDriftJumpCount]; .... }; void VCMRttFilter::Reset() { _gotNonZeroUpdate = false; _avgRtt = 0; _varRtt = 0; _maxRtt = 0; _filtFactCount = 1; _jumpCount = 0; _driftCount = 0; memset(_jumpBuf, 0, kMaxDriftJumpCount); memset(_driftBuf, 0, kMaxDriftJumpCount); }
ここでは、配列の最初の要素と2番目の要素の1バイトのみがゼロにリセットされます。
PVS-Studio警告:V512 CWE-682 'memset'関数を呼び出すと、バッファー '_jumpBuf'がアンダーフローします。 rtt_filter.cc 52
提言
このようなエラーを回避する方法は、 memset以上を使用しないことです。 細心の注意を払うこともできますが、遅かれ早かれエラーはプロジェクトに漏れます。 これは、Chromiumプロジェクトの良い写真です。 他のプロジェクトでは、これは非常に一般的な問題です( 証明 )。
はい、Cコードでmemsetを拒否することはできません。 ただし、C ++について説明している場合は、この関数について忘れましょう。 C ++コードではmemset関数を使用しないでください。 ドットを使用しないでください。
memset呼び出しを何に置き換えますか?
最初に、 std :: fill関数を使用できます。 この場合、配列の入力は次のようになります。
fill(begin(key_event->text), end(key_event->text), 0);
第二に、多くの場合、特別な関数呼び出しをまったく使用する必要はありません。 原則として、 memset関数はローカルの配列と構造を初期化するのに役立ちます。 クラシック:
HDHITTESTINFO hhti; memset(&hhti, 0, sizeof(hhti));
しかし、はるかに簡単で信頼性の高い記述ができます。
HDHITTESTINFO hhti = {};
コンストラクタについて話している場合:
class C { int A[100]; public: C() { memset(A, 0, sizeof(A)); } };
それは書くことができます:
class C { int A[100] = {}; public: C() { } };
memsetからの間違った期待
時々、2番目の引数が1バイトの値を設定することを忘れてしまいます。これは、バッファを埋めるために使用されます。 memset関数の2番目の引数がint型であることは紛らわしいです。 結果として、 CWE-628 :誤って指定された引数を持つ関数呼び出しとして分類できるエラーが発生します。
Chromiumプロジェクトで使用されているV8エンジンで気付いた同様のエラーの例を見てみましょう。
void i::V8::FatalProcessOutOfMemory( const char* location, bool is_heap_oom) { .... char last_few_messages[Heap::kTraceRingBufferSize + 1]; char js_stacktrace[Heap::kStacktraceBufferSize + 1]; i::HeapStats heap_stats; .... memset(last_few_messages, 0x0BADC0DE, Heap::kTraceRingBufferSize + 1); memset(js_stacktrace, 0x0BADC0DE, Heap::kStacktraceBufferSize + 1); memset(&heap_stats, 0xBADC0DE, sizeof(heap_stats)); .... }
PVS-Studioの警告:
- V575 CWE-628 'memset'関数は値 '195936478'を処理します。 2番目の引数を調べます。 api.cc 327
- V575 CWE-628 'memset'関数は値 '195936478'を処理します。 2番目の引数を調べます。 api.cc 328
- V575 CWE-628 'memset'関数は値 '195936478'を処理します。 2番目の引数を調べます。 api.cc 329
プログラマーは、デバッグ中に何が起こっているのかを理解しやすくするために、メモリブロックに値0x0BADC0DEを入力することを決定しました。 ただし、メモリ領域は0xDEの値を持つバイトで埋められます。
プログラマーがコードで行うことは低レベルの操作であり、ここでmemsetなしで行うことは、前述の状況よりも困難です。 バッファーのサイズは4バイトの倍数ではないため、 std :: fillを使用する前のようには機能しません。 独自の関数を作成して使用する必要があります。
void Fill_0x0BADC0DE(void *buf, const size_t size) { const unsigned char badcode[4] = { 0xDE, 0xC0, 0xAD, 0x0B }; size_t n = 0; generate_n(static_cast<char *>(buf), size, [&] { if (n == 4) n = 0; return badcode[n++]; }); }
提言
ここには特別な推奨事項はありません。 しかし、プログラマーに割り当てられたタスクを解決しないため、ここではmemset関数は実際には必要ないと確信しました。
個人データの上書きエラー
memset関数は、不要になったプライベートデータを消去するために使用されます。 これは間違っています。 memset関数を呼び出した後にプライベートデータを含むバッファが使用されない場合、コンパイラはこの関数の呼び出しを削除する権利を持っています。 この欠陥はCWE-14に分類されます:バッファをクリアするコードのコンパイラ削除。
コンパイラへのmemset呼び出しを削除できないという異議をすでに予想しています。 できます。 そして彼は最適化を目標にしています。 このトピックを理解するために、次の記事「 プライベートデータの安全なクリーニング 」を注意深く学習することを提案します。
このようなエラーが実際にどのように見えるかを見てみましょう。 Chromiumで使用されるWebRTCライブラリから始めます。
void AsyncSocksProxySocket::SendAuth() { .... char * sensitive = new char[len]; pass_.CopyTo(sensitive, true); request.WriteString(sensitive); // Password memset(sensitive, 0, len); delete [] sensitive; DirectSend(request.Data(), request.Length()); state_ = SS_AUTH; }
PVS-Studio警告: V597 CWE-14コンパイラーは、「機密」オブジェクトをフラッシュするために使用される「memset」関数呼び出しを削除できました。 RtlSecureZeroMemory()関数を使用して、プライベートデータを消去する必要があります。 socketadapters.cc 677
100%に近い確率のmemset関数は、リリースバージョンのコンパイラによって削除されます。
アヤヤイ! パスワードはメモリのどこかにぶら下がったままで、理論的にはどこかに送信できます。 真剣に、それは本当に起こります。
同じライブラリで、さらに3つの同様のエラーに遭遇しました。 それらは同じタイプなので、説明しません。 アナライザーからの対応するメッセージのみを提供します。
- V597 CWE-14コンパイラーは、「memset」関数呼び出しを削除できます。これは、「sensitive」オブジェクトをフラッシュするために使用されます。 RtlSecureZeroMemory()関数を使用して、プライベートデータを消去する必要があります。 httpcommon.cc 721
- V597 CWE-14コンパイラーは、「memset」関数呼び出しを削除できます。これは、「sensitive」オブジェクトをフラッシュするために使用されます。 RtlSecureZeroMemory()関数を使用して、プライベートデータを消去する必要があります。 httpcommon.cc 766
- V597 CWE-14コンパイラーは、「memset」関数呼び出しを削除できます。これは、「sensitive」オブジェクトをフラッシュするために使用されます。 RtlSecureZeroMemory()関数を使用して、プライベートデータを消去する必要があります。 httpcommon.cc 917
提言
memset関数を使用して個人データを消去しないでください!
コードの最適化中にコンパイラによって削除できない特殊なメモリクリーニング関数を使用する必要があります。
ご注意 これは、C ++プログラマーだけでなく、Cプログラマーにも当てはまります。
たとえば、Visual Studioでは、 RtlSecureZeroMemoryを使用できます。 C11以降、 memset_s関数があります。 必要に応じて、独自の安全な関数を作成できます。 インターネットには、その作成方法の例が数多くあります。 以下にいくつかのオプションを示します。
オプションN1
errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n) { if (v == NULL) return EINVAL; if (smax > RSIZE_MAX) return EINVAL; if (n > smax) return EINVAL; volatile unsigned char *p = v; while (smax-- && n--) { *p++ = c; } return 0; }
オプションN2
void secure_zero(void *s, size_t n) { volatile char *p = s; while (n--) *p++ = 0; }
Chromiumプロジェクトの場合、おそらくOPENSSL_cleanse関数を使用するのが合理的です。
おわりに
C ++プログラムを作成していて、 memset関数呼び出しを作成する場合は、停止します。 ほとんどの場合、この危険な機能がなくてもうまくいくでしょう。
この記事を英語圏の聴衆と共有したい場合は、翻訳へのリンクを使用してください:Andrey Karpov。 素敵なChromiumと不器用なmemset 。