いくつかのErlang/OTPモジュールのコードは、ほとんどの近代的なジュニア開発者よりも古い。これらのファイルは真のデジタルパトリアルである。何十年もの間、彼らは、銀行取引、電話ネットワーク、メッセージシステムがすべて適切に機能していることを確認してきました。我々は、今日の何百万ものユーザーが信頼しているラインの背後に何があるかを見るために、この長年の言語のキャップの下を見ることにしました。 導入 警告について話し合う前に、プロジェクトについて少し言いたい。 Erlangの旅は1986年にエリクソンの研究所の1つで始まったが、ジョー・アームストロングは電話プログラムを構築しようとしながらプロログを実験していた。 失敗が受け入れられないシステムのために設計されたプログラミング言語です. 電話のスイッチを思い浮かべてください. 落とされた通話が緊急事態として数えられるか、何があっても通らなければならない銀行取引です. これらはそのために使われているタスクです. それは、システムが何百万もの孤立した独立したプロセスに分裂することを可能にします。 エルラン Erlang は、その手と共に、 この組み込みのツールキットは、分散型、エラートレントなアプリケーションを構築するための強力なツール、パターン、および行動を提供します。 オープン・テレコム・プラットフォーム(OTP) このプロジェクトに従って作り上げた でチェックしてみました♪ Branch 利用 そして、The 私たちはプロジェクトの構築に問題を抱えていなかったので、開発者にクドー。 指示 maint-28 PVS-Studio analyzer Visual Studio コード この記事には、高級および中級レベルの警告のみが含まれています。 これで私たちの紹介が終わります。今、警告に進み、私たちはそれらを3つのカテゴリーに分けます: 論理的な誤り 批判的な過ち メモリエラー 論理ミス Fragment N1 PVSスタジオの警告: 「(!esock_is_integer((env), (argv[3])))」のオペレーターの左と右に同じサブ表現があります。 V501 prim_socket_nif.c 6035 static ERL_NIF_TERM nif_sendfile(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { .... BOOLEAN_T a2ok; if ((! (a2ok = GET_INT64(env, argv[2], &offset64))) || (! GET_UINT64(env, argv[3], &count64u))) { if ((! IS_INTEGER(env, argv[3])) || (! IS_INTEGER(env, argv[3]))) return enif_make_badarg(env); if (! a2ok) return esock_make_error_integer_range(env, argv[2]); else return esock_make_error_integer_range(env, argv[3]); } .... } 全画面モード 全画面モード 全画面モード 全画面モード コードでは、開発者は3番目と4番目のアルゴリズムを64ビット型(それぞれ署名されたアルゴリズムと未署名のアルゴリズム)に変換しようとした。 固定コード: .... if ((! IS_INTEGER(env, argv[2])) || (! IS_INTEGER(env, argv[3]))) return enif_make_badarg(env); .... 全画面モード 全画面モード 全画面モード 全画面モード Fragment N2 PVSスタジオの警告: 「n->type == ycf_node_type_on_save_yield_state_code」の左と右に同じサブ表現があります。 V501 ycf_node.c 218 static void uniqify_local_vars_in_node(ycf_node* n) { if (n->type == ycf_node_type_code_scope) { uniqify_local_vars_in_scope(&n->u.code_scope); } else if(n->type == ycf_node_type_on_restore_yield_state_code) { uniqify_local_vars_in_scope(&n->u.special_code_block .code.if_statement->u.code_scope); } else if (n->type == ycf_node_type_on_save_yield_state_code || n->type == ycf_node_type_on_save_yield_state_code || n->type == ycf_node_type_on_destroy_state_code || n->type == ycf_node_type_on_return_code || n->type == ycf_node_type_on_destroy_state_or_return_code) { uniqify_local_vars_in_scope(&n->u.special_code_block .code.if_statement->u.code_scope); } .... } 全画面モード 全画面モード 全画面モード 全画面モード もう一つ興味深いコピーペストケースがあります。 ツリーノードを回復的に処理し、局所変数を有する可視性領域を見つける機能で、それからこれらの領域のそれぞれのための変数名統一機能を呼び出す。 ブランド、The 条件は、その後の倍増です。 オペレーター uniqify_local_vars_in_node else if n->type == ycf_node_type_on_save_yield_state_code || 比較チェーンで判断すると、次のリストの値は、宣言された順に分類されます: typedef enum { .... ycf_node_type_on_save_yield_state_code, ycf_node_type_on_restore_yield_state_code, ycf_node_type_on_destroy_state_code, ycf_node_type_on_return_code, ycf_node_type_on_destroy_state_or_return_code } ycf_node_type; 全画面モード 全画面モード 全画面モード 全画面モード しかし、The constant は、前回の だから、結局ここに二重チェックがあると思うし、それを削除できる。 ycf_node_type_on_restore_yield_state_code else if Fragment N3 PVSスタジオの警告: Possible typo in the spelling of a pre-defined macro name. The ' 「マクロは似ている」 「。 V1040 WIN_32 WIN32 inet_drv.c 13506 static int tcp_send_error(tcp_descriptor* desc, int err) { /* EPIPE errors usually occur in one of three ways: * 1. We write to a socket when we've already shutdown() the write side. * On Windows the error returned for this is ESHUTDOWN * rather than EPIPE. * 2. The TCP peer sends us an RST through no fault of our own (perhaps * by aborting the connection using SO_LINGER) and we then attempt * to write to the socket. On Linux and Windows we would actually * receive an ECONNRESET error for this, but on the BSDs, Darwin, * Illumos and presumably Solaris, it's an EPIPE. * 3. We cause the TCP peer to send us an RST by writing to a socket * after we receive a FIN from them. Our first write will be * successful, but if the they have closed the connection (rather * than just shutting down the write side of it) this will cause their * OS to send us an RST. Then, when we attempt to write to the socket * a second time, we will get an EPIPE error. On Windows we get an * ECONNABORTED. * * What we are going to do here is to treat all EPIPE messages that aren't * of type 1 as ECONNRESET errors. This will allow users who have the * show_econnreset socket option enabled to receive {error, econnreset} on * both send and recv operations to indicate that an RST * has been received. */ #ifdef __WIN_32__ if (err == ECONNABORTED) err = ECONNRESET; #endif if (err == EPIPE && !(desc->tcp_add_flags & TCP_ADDF_SHUTDOWN_WR_DONE)) err = ECONNRESET; return tcp_send_or_shutdown_error(desc, err); } 全画面モード 全画面モード 全画面モード 全画面モード これは、プロジェクトが Windows でコンパイルするかどうかをチェックする比較的珍しいケースです。 ( ) ( ) ( ) ( ) ], etc.), but this one is unique. This looks very much like a simple typo (most likely, it should be ( ) 1 2 3 __WIN32__ なんでこんなの? 好奇心から探してみました。 見てみたい!結果はこちら。 "__WIN_32__" As you can see, there are almost no results. It's funny that one of the links leads to the file we're looking at on . 私はこのプロジェクトを通して、類似のマクロ定義を見つけることができませんでした. I also thought it might be built into some compiler, like MinGW. 残念ながら、 マクロの目的を理解しようとすると、このコードが欠陥だと信じています。 Debian Sources 実験 修正として、プロジェクトの他の部分で以前見つけたチェックを使用することをお勧めします。 static int tcp_send_error(tcp_descriptor* desc, int err) { .... #if defined(__WIN32__) || defined(_WIN32) || defined(_WIN32_) if (err == ECONNABORTED) err = ECONNRESET; #endif .... } 全画面モード 全画面モード 全画面モード 全画面モード Fragment N4 The PVS-Studio warning: コードの運用論理は、その形式化と一致しません。第2の文は常に実行されます。カラフルなブレイクが欠けている可能性があります。 V640 ダイヤモンドC 246 int main(int argc, char** argv) { .... if (argc > 2 && strcmp(argv[1], "+P") == 0) { PUSH2("+P", argv[2]); argc--, argv++; argc--, argv++; } else PUSH2("+P", "1000000"); .... } 全画面モード 全画面モード 全画面モード 全画面モード 以下は、後で発見するのが困難な一般的なコーディングエラーの興味深い例です。 構造: PUSH2 #define QUOTE(s) s #define PUSH(s) eargv[eargc++] = QUOTE(s) #define PUSH2(s, t) PUSH(s); PUSH(t) 全画面モード 全画面モード 全画面モード 全画面モード Uh-huh, it's a macro that expands into two constructs. Let's expand the code as a preprocessor would: プロセッサとしてコードを拡張しましょう。 int main(int argc, char** argv) { .... if (argc > 2 && strcmp(argv[1], "+P") == 0) { eargv[eargc++] = "+P"; eargv[eargc++] = argv[2]; argc--, argv++; argc--, argv++; } else eargv[eargc++] = "+P"; eargv[eargc++] = "1000000"; .... } 全画面モード 全画面モード 全画面モード 全画面モード 結果として、検査の結果は、 , only the first assignment will be conditionally executed, while the second one will always occur after the branch. これはほとんど開発者が達成したいことではありません。 false マクロの作成と使用 . 完璧なソリューションは、それらを除去することになりますが、少なからず修正するようにしましょう。 複数のアクションをマクロ内の 1 つに組み合わせる必要があります。 バグを惹きつける do {...} while (0) #define PUSH2(s, t) do { PUSH(s); PUSH(t); } while (0) 全画面モード 全画面モード 全画面モード 全画面モード Fragment N5 PVSスタジオの警告: ループの条件「i>=0」は常に正しい。 V654 wxe_return.cpp 236 ERL_NIF_TERM wxeReturn::make_array_objs(wxAuiPaneInfoArray& arr, WxeApp *app, const char *cname) { ERL_NIF_TERM head, tail; ERL_NIF_TERM class_name = enif_make_atom(env, cname); tail = enif_make_list(env, 0); for (unsigned int i = arr.GetCount() - 1; i >= 0; i--) { head = make_ref(app->getRef((void *) &arr.Item(i),memenv), class_name); tail = enif_make_list_cell(env, head, tail); } return tail; } 全画面モード 全画面モード 全画面モード 全画面モード 見てみよう The 特に注目すべきは、その状態から、 Induction variable is of the type with a value range of , the loop becomes infinite. After the loop iterates with , the variable becomes equal to 減少の結果、循環は続きます。 for i unsigned int [0; UINT_MAX] i == 0 UINT_MAX 以下のようにコードを修正します。 for (int64_t i = arr.GetCount() - 1; i >= 0; i--) { .... } 全画面モード 全画面モード 全画面モード 全画面モード 似たような警告: V654 ループの条件 'i >= 0' は常に正しい。 V654 ループの条件 'i >= 0' は常に正しい。 Fragment N6 PVSスタジオの警告: 「もし(A) {...} その他(A) {...}」のパターンの使用が検出された場合、論理的なエラーの存在の可能性があります。チェックライン: 2903, 3053. V517 erl_bif_info.c 2903 BIF_RETTYPE system_info_1(BIF_ALIST_1) { .... if (is_tuple(BIF_ARG_1)) { // L2778 .... } else if (BIF_ARG_1 == am_scheduler_id) { // L2782 .... } .... else if (BIF_ARG_1 == am_garbage_collection) { // L2903 .... } else if (BIF_ARG_1 == am_fullsweep_after) { // L2921 .... } else if (BIF_ARG_1 == am_garbage_collection) { // L3053 .... } else if (BIF_ARG_1 == am_instruction_counts) { // L3056 .... } .... else if (ERTS_IS_ATOM_STR("halt_flush_timeout", BIF_ARG_1)) { // L3552 .... } } 全画面モード 全画面モード 全画面モード 全画面モード アナリストは、膨大な数を含む関数で同一のチェックを持ついくつかの支店を検出しました。 コメント - ほぼ しかし、それぞれ異なる論理を持っている: そして . 支店の数と複製物の間の150行間のギャップを考えると、これは起こりうるということはほとんど驚きではありません。 静的分析はそのようなケースを防ぐのに役立ちます。 残念ながら、修正は考えられないので、開発者にそれを残します。 if-else if 800 長いライン first check 第2次チェック Fragment N7 PVSスタジオの警告: A part of conditional expression is always false: (arity == 0) V560 ビッグC 3145 BIF_RETTYPE delete_element_2(BIF_ALIST_3) { .... ptr = tuple_val(BIF_ARG_2); arity = arityval(*ptr); ix = signed_val(BIF_ARG_1); if ((ix < 1) || (ix > arity) || (arity == 0)) { BIF_ERROR(BIF_P, BADARG); } .... } Enter fullscreen mode Exit fullscreen mode ここで、私たちは数学に関わる例を持っています. The analyzer informs us that the last サブ表現は常に偽物です なぜ? arity == 0 コントロールフローがこのサブ表現を評価するポイントに達した場合、前2つのサブ表現が . We can also learn the following: false ix >= 1 ix <= arity Enter fullscreen mode Exit fullscreen mode これらの表現を数学的に書き換えましょう: 1 <= ix <= arity 全画面モード 全画面モード 全画面モード 全画面モード Next, we bring up a , which states that だから、 この文脈では決して0ではない。 過渡関係のルール arity >= 1 arity I'll also leave fixing this code to the developers, since my only suggestion is to remove the condition. 批判的な過ち Fragment N8 PVSスタジオの警告: 「prefix_len」インデックスの値は262に達する可能性があります。 ワイ557 erl_alloc_util.c 5375 Array overrun is possible. The value of 'prefix_len' index could reach 262. ワイ557 erl_alloc_util.c 5378 「prefix_len」インデックスの値は262に達する可能性があります。 ワイ557 erl_alloc_util.c 5381 #define MAX_ATOM_CHARACTERS 255 static void make_name_atoms(Allctr_t *allctr) { char alloc[] = "alloc"; char realloc[] = "realloc"; char free[] = "free"; char buf[MAX_ATOM_CHARACTERS]; size_t prefix_len = sys_strlen(allctr->name_prefix); if (prefix_len > MAX_ATOM_CHARACTERS + sizeof(realloc) - 1) erts_exit(ERTS_ERROR_EXIT, "Too long allocator name: %salloc\n" allctr->name_prefix); sys_memcpy((void *) buf, (void *) allctr->name_prefix, prefix_len); sys_memcpy((void *) &buf[prefix_len], (void *) alloc, sizeof(alloc) - 1); allctr->name.alloc = am_atom_put(buf, prefix_len + sizeof(alloc) - 1); sys_memcpy((void *) &buf[prefix_len], (void *) realloc, sizeof(realloc) - 1); allctr->name.realloc = am_atom_put(buf, prefix_len + sizeof(realloc) - 1); sys_memcpy((void *) &buf[prefix_len], (void *) free, sizeof(free) - 1); allctr->name.free = am_atom_put(buf, prefix_len + sizeof(free) - 1); } 全画面モード 全画面モード 全画面モード 全画面モード We have a function that generates names for allocation and deallocation functions based on the prefix passed to it. A 255-byte buffer is used to generate them. Judging by the function contents, there will be no null terminator in the generated buffer, since passes the total size of the generated string. am_atom_put 開発者が最初にしたことは、プレフィックスのサイズを計算することでした. 次に、彼らはプレフィックスのサイズを制限しました。 そして、その長さは string (the longest postfix) は、 function is called. MAX_ATOM_CHARACTERS realloc noreturn 次に、彼らはバッファにプレフィックスをコピーし、ポストフィックスを追加し、文字列をバッファに渡しました。 機能. The develops performed all these actions sequentially for all postfixes. 開発者は、すべてのポストフィックスに対して、次のすべての操作を実行した。 で、 , and . am_atom_put alloc realloc free 捕獲を確認できますか? 早期返信チェックは間違って行われました。 バッファには、最長のポストフィックス、すなわち7文字を書くのに十分なスペースが必要です。 プレフィックス文字列が256〜262文字の間の長さの場合、早期返信は発生しません。 function overflows the buffer. This operation has undefined behavior. sys_memcpy 仮定すると、The function works with strings no longer than 以下のようにコードを修正します。 am_atom_put MAX_ATOM_CHARACTERS if (prefix_len > MAX_ATOM_CHARACTERS - sizeof(realloc) + 1) 全画面モード 全画面モード 全画面モード 全画面モード Now, the maximum prefix length is 248 characters. When writing the maximum postfix, overflow won't occur. Similar warnings: V557 Array overrun is possible. The value of 'i' index could reach 40. ycf_lexer.c 493 V557 Array overrun is possible. The value of 'n' index could reach 16. erl_call.c 1663 Fragment N9 I'd like to introduce this example in an unusual way. First, let's look at this function: static const char* event_state_flag_to_str(EventStateFlags f, const char *buff, int len) { switch ((int)f) { case ERTS_EV_FLAG_CLEAR: return "CLEAR"; case ERTS_EV_FLAG_USED: return "USED"; /* other cases that return string literals */ default: snprintf((char *)buff, len, "ERROR(%d)", f); return buff; } } Enter fullscreen mode Exit fullscreen mode THE 機能が変換する ストリップを1つにまとめて、非... branches, 文字列を文字列に返します. でなければ、関数は提供されたバッファに文字列を作成し、それにポインターを返します. バッファがこのことを正確にどのように行うかを注意してください: バッファは強制的にコンスタンスを排除します. 標準Cによると、これはソースバッファが宣言されていない場合にのみ許可されます。 (C11 , C23 ). event_state_flag_to_str EventStateFlags default switch const 6.7.3.6 6.7.4.1.7 Is this really the case? Let's take a look at the invocation point: static ERTS_INLINE void print_flags(erts_dsprintf_buf_t *dsbufp, EventStateFlags f) { const char buff[64]; if (f & ERTS_EV_FLAG_WANT_ERROR) { erts_dsprintf(dsbufp, "WANTERR|"); f &= ~ERTS_EV_FLAG_WANT_ERROR; } erts_dsprintf(dsbufp, "%s", event_state_flag_to_str(f, buff, sizeof(buff))); } Enter fullscreen mode Exit fullscreen mode この関数は、常時バッファを第2の議論として渡すことで呼ばれます。残念ながら、未定義の動作が発生しました。このエラーは、アナリストを使用して見つかりましたが、混乱する警告を発信しました。 Uninitialized buffer 'buff' used. Consider checking the second actual argument of the 'event_state_flag_to_str' function. バッファの初期化は「buff」を使用しています。 V614 erl_check_io.c 2833 Let's fix the code by removing the constness from the buffer: static ERTS_INLINE void print_flags(erts_dsprintf_buf_t *dsbufp, EventStateFlags f) { char buff[64]; if (f & ERTS_EV_FLAG_WANT_ERROR) { erts_dsprintf(dsbufp, "WANTERR|"); f &= ~ERTS_EV_FLAG_WANT_ERROR; } erts_dsprintf(dsbufp, "%s", event_state_flag_to_str(f, buff, sizeof(buff))); } 全画面モード 全画面モード 全画面モード 全画面モード 改めることもお勧めです。 function signature, so the parameter can take a pointer. This makes it clear that the function can modify the buffer passed to it. We won't be breaking much code in the process since the function exists only in the compiled file and is marked as . event_state_flag_to_str char* static Fragment N10 The PVS-Studio warning: The uninitialized class member 'a' is used when initializing the base class 'BeamAssemblerCommon'. V1050 beam_asm.hpp 87 class BeamAssemblerCommon : public ErrorHandler { BaseAssembler &assembler; protected: BeamAssemblerCommon(BaseAssembler &assembler); .... }; struct BeamAssembler : public BeamAssemblerCommon { BeamAssembler() : BeamAssemblerCommon(a) { /* .... */ } protected: a64::Assembler a; .... }; Enter fullscreen mode Exit fullscreen mode This fragment contains a small class hierarchy where a reference to a non-static member function of the derived class, , は、ベースクラスの構築者に転送され、 この操作は、ベースクラスの構築者がこのメンバー関数と相互作用しない限り、問題を引き起こしません。 なぜ? 由来クラスのオブジェクトのライフタイムは、初期化リストの必要な初期化者が実行されたとき、または、初期化者が存在しない場合、制御フローが構築体に入ったときに始まります。 BeamAssembler BeamAssemblerCommon この情報を念頭に置いて、ベースクラスコンクリエーターを見ていきましょう. It doesn't use this object, right? BeamAssemblerCommon::BeamAssemblerCommon(BaseAssembler &assembler_) : assembler(assembler_), .... { .... #ifdef DEBUG assembler.addDiagnosticOptions(DiagnosticOptions::kValidateAssembler); #endif assembler.addEncodingOptions(EncodingOptions::kOptimizeForSize | EncodingOptions::kOptimizedAlign); .... } 全画面モード 全画面モード 全画面モード 全画面モード Nope :) Non-static member function is called on an object of a derived class via a reference when its lifetime has not yet begun. Again, we get undefined behavior. 非静的メンバー関数は、そのライフタイムがまだ始まっていないときに、リファレンスを通じて、由来クラスのオブジェクトに呼び出されます。 私もこの場合の修正を提案するのは難しいので、それを開発者に残します。 メモリエラー Fragment N11 The PVS-Studio warning: The 'free' function is called twice for deallocation of the same memory space. V586 トップページ > C668 int main(int argc, char *argv[]) { int fd; char *p = NULL; ei_cnode ec; .... int i = 0; ei_x_buff reply; .... if (ei_rpc(&ec, fd, "c", "c", p, i, &reply) < 0) { free(p); ei_x_free(&reply); fprintf(stderr,"erl_call: can't compile file %s\n", fname); } free(p); /* FIXME complete this code FIXME print out error message as term if (!erl_match(erl_format("{ok,_}"), reply)) { fprintf(stderr,"erl_call: compiler errors\n"); } */ ei_x_free(&reply); .... } 全画面モード 全画面モード 全画面モード 全画面モード 名前で判断すると、 is a . A negative number is returned if the operation fails. If the remote call fails, some resources are cleaned up. The pointer は、the function and an error is logged to . Next, regardless of the function call result, the pointer is passed to again. Memory is released twice. ei_rpc remote procedure call p free stderr free 修正は非常に簡単です:プログラムの実行を停止するか、リリースポインタをゼロに設定します。 if (ei_rpc(&ec, fd, "c", "c", p, i, &reply) < 0) { free(p); ei_x_free(&reply); fprintf(stderr,"erl_call: can't compile file %s\n", fname); return 1; } free(p); 全画面モード 全画面モード 全画面モード 全画面モード Fragment N12 PVSスタジオの警告: 「pkey」ポインターは、nullptrに対して検証される前に使用された。 ワイ595 トップページ > 581 static int get_pkey_public_key(ErlNifEnv *env, const ERL_NIF_TERM argv[], int algorithm_arg_num, int key_arg_num, EVP_PKEY **pkey, ERL_NIF_TERM *err_return) { .... if (enif_is_map(env, argv[key_arg_num])) { password = get_key_password(env, argv[key_arg_num]); *pkey = ENGINE_load_public_key(e, id, NULL, password); if (!pkey) assign_goto(*err_return, err, EXCP_BADARG_N(env, key_arg_num, "Couldn't get public key from engine")); } /* other branches */ ret = 1; done: .... return ret; err: .... *pkey = NULL; ret = 0; goto done; } Enter fullscreen mode Exit fullscreen mode We have quite an interesting code fragment here. The 公開鍵を第5のパラメータに抽出しようとします。 成功し、そして if not. In the latter case, the public key is zeroed as well. function 1 0 アナリストは、機能の分野の1つに、 ポインタは有効性をチェックする前に再参照されます。しかし、他の支店はそのようなチェックを持っていないように見えます。我々は、この機能には次の契約があると結論づけることができます。 parameter must never be equal to a ." We can confirm this by searching for calls to this function in the same file (この機能への呼び出しを同じファイルで検索することによって確認できます。 ], [ ( ) ( ) ])—the variable address is passed everywhere. pkey pkey nullptr 1 2 3 In fact, the developers intended to handle the incorrect result of the function by jumping to the しかし、彼らは星座の前に星座を置くことを忘れて、文字を書いた。 現代のコンピュータは、 this check on release. As a result, the function will return , and a null pointer will end in the fifth parameter. , and a null pointer will end in the fifth parameter. ENGINE_load_public_key err ! optimize away 1 チェックを整えましょう: .... *pkey = ENGINE_load_public_key(e, id, NULL, password); if (!*pkey) assign_goto(*err_return, err, EXCP_BADARG_N(env, key_arg_num, "Couldn't get public key from engine")); .... Enter fullscreen mode Exit fullscreen mode Fragment N13 We often receive messages saying that the analyzer warns about passing a null pointer to . After all, the standard states that nothing terrible will happen. We even wrote an about a similar case. free 記事 はい、それは大丈夫ですが、そのようなコードはおそらく疑わしいでしょう. それは私たちにコード内の興味深いメモリ漏洩を見つけるのに役立ちました. それを見てみましょう. PVSスタジオの警告: The null pointer is passed into 'free' function. Inspect the first argument. ワイ575 erl_misc_utils.c 264 void erts_cpu_info_destroy(erts_cpu_info_t *cpuinfo) { if (cpuinfo) { cpuinfo->configured = 0; cpuinfo->online = 0; cpuinfo->available = 0; #ifdef HAVE_PSET_INFO if (cpuinfo->cpuids) free(cpuinfo->cpuids); #endif cpuinfo->topology_size = 0; if (cpuinfo->topology) { cpuinfo->topology = NULL; free(cpuinfo->topology); } free(cpuinfo); } } Enter fullscreen mode Exit fullscreen mode この部分では、The pointer is explicitly set to 電話する前に したがって、メモリの解放操作は無意味であるため、 does nothing. This results in memory leaks because the original memory block that pointed to is never freed. cpuinfo->topology NULL free free(NULL) cpuinfo->topology 操作を交換することによって問題を解決することができます: if (cpuinfo->topology) { free(cpuinfo->topology); cpuinfo->topology = NULL; } 全画面モード 全画面モード 全画面モード 全画面モード Fragment N14 The PVS-Studio warning: この機能は「入力」ポインタをリリースすることなく終了しました. メモリ漏洩が可能です。 V773 wxe_gl.cpp 88 typedef struct _wxe_glc { wxGLCanvas *canvas; wxGLContext *context; } wxe_glc; void setActiveGL(wxeMemEnv *memenv, ErlNifPid caller, wxGLCanvas *canvas, wxGLContext *context) { ErlNifUInt64 callId = wxe_make_hash(memenv->tmp_env, &caller); wxe_glc * entry = glc[callId]; gl_active_index = callId; gl_active_pid = caller; if (!entry) { entry = (wxe_glc *) malloc(sizeof(wxe_glc)); entry->canvas = NULL; entry->context = NULL; } if (entry->canvas == canvas && entry->context == context) return; entry->canvas = canvas; entry->context = context; glc[gl_active_index] = entry; .... } 全画面モード 全画面モード 全画面モード 全画面モード In the 機能、The ポインタが宣言され、マレー要素を介して初期化されます. If this element is a null pointer, memory is allocated using . Next, the pointers are compared. If it evaluates to しかし、ダイナミックな割り当てが発生した場合、メモリは解放されません。 setActiveGL entry malloc true はい、そのような状況はかなり稀で、それは一連の出来事を必要とするため、起こる可能性は低い: 関数が呼び出されたとき、ゼロポインタは3番目と4番目のアルゴリズムとして渡されます。 glc[callId] には null ポインターが含まれています。 However, we can ensure that a leak will never occur, not even theoretically. Let's modify the code a little: ErlNifUInt64 callId = wxe_make_hash(memenv->tmp_env, &caller); wxe_glc * entry = glc[callId]; gl_active_index = callId; gl_active_pid = caller; bool new_alloc = false; if (!entry) { entry = (wxe_glc *) malloc(sizeof(wxe_glc)); entry->canvas = NULL; entry->context = NULL; new_alloc = true; } if (entry->canvas == canvas && entry->context == context) { if (new_alloc) { free(entry); } return; } .... 全画面モード 全画面モード 全画面モード 全画面モード 似たような警告: V773 この関数は「f」ハンドルで参照されたファイルを閉じることなく終了しました。 V773 The 'erl_errno_p' pointer was assigned values twice without releasing the memory. A memory leak is possible. ei_pthreads.c 195 V773 この関数は、「fp」ハンドルで参照されたファイルを閉じることなく終了しました。 V773 例外は「データ」ポインターをリリースせずに投げ捨てられた。 wxe_wrapper_4.cpp 1300 V773 例外は「データ」ポインターをリリースせずに投げ捨てられた。 wxe_wrapper_4.cpp 1330 V773 The exception was thrown without releasing the 'data' pointer. A memory leak is possible. wxe_wrapper_4.cpp 1663 V773 The exception was thrown without releasing the 'data' pointer. A memory leak is possible. wxe_wrapper_4.cpp 1690 V773 The exception was thrown without releasing the 'data' pointer. A memory leak is possible. wxe_wrapper_4.cpp 1715 V773 The exception was thrown without releasing the 'alpha' pointer. A memory leak is possible. wxe_wrapper_4.cpp 1718 V773 The exception was thrown without releasing the 'data' pointer. A memory leak is possible. wxe_wrapper_4.cpp 1747 V773 例外は「alpha」ポインターをリリースせずに投げ捨てられた。 wxe_wrapper_4.cpp 1750 V773 The exception was thrown without releasing the 'alpha' pointer. A memory leak is possible. wxe_wrapper_4.cpp 2680 V773 例外は「データ」ポインターをリリースせずに投げ捨てられた。 wxe_wrapper_4.cpp 2715 V773 The exception was thrown without releasing the 'data' pointer. A memory leak is possible. wxe_wrapper_4.cpp 2733 Fragment N15 PVSスタジオの警告: realloc() possible leak: when realloc() fails in allocating memory, original pointer 'outbuf_base' is lost. Consider allocing realloc() to a temporary pointer. realloc() possible leak: realloc() fails in allocating memory, original pointer 'outbuf_base' is lost. V701 トップ > トップ > 1324 static void outbuf_append(const char* buf, int n) { .... /* * Allocate a larger buffer if we still cannot fit the data. */ if (outbuf_base+outbuf_total < outbuf_in+n) { int size = outbuf_in - outbuf_out; outbuf_total = size+n; outbuf_base = realloc(outbuf_base, outbuf_total); outbuf_out = outbuf_base; outbuf_in = outbuf_base + size; } .... } 全画面モード 全画面モード 全画面モード 全画面モード There is a tricky pattern with the オリジナルのポインターが返す値によって直ちに書かれている機能で、これの底まで行きましょう。 realloc この関数は、再割り当て用のバッファを最初のアルバムとして、新しいバッファサイズを2番目のアルバムとして使用します. If the operation is successful, the function will return a pointer to the newly allocated memory. たとえ単純なメモリブロックの拡張のみが発生したとしても、この操作が失敗した場合、 記憶は、最初の議論が残るように過ぎ去った。 . invalid null pointer untouched So, if we immediately overwrite the original pointer with a call to , we will get a memory leak. realloc Let's fix the code as follows: static void outbuf_append(const char* buf, int n) { .... /* * Allocate a larger buffer if we still cannot fit the data. */ if (outbuf_base + outbuf_total < outbuf_in + n) { int size = outbuf_in - outbuf_out; outbuf_total = size+n; char *tmp = realloc(outbuf_base, outbuf_total); if (!tmp) { /* somehow handle this scenario * the `outbuf_base` buffer here is still valid */ } outbuf_base = tmp; outbuf_out = outbuf_base; outbuf_in = outbuf_base + size; } .... } 全画面モード 全画面モード 全画面モード 全画面モード 固定した例では、私たちはまた、状況に対処する必要があります。 returns the null pointer. This may be an exception throwing, an attempt to allocate a buffer of a different size, and so on. realloc Fragment N16 PVSスタジオの警告: The 'end' pointer in the 'end - mm->sua.top' expression equals nullptr. The resulting value is senseless and it should not be used. ワイ769 erl_mmap.c 2411 static void add_free_desc_area(ErtsMemMapper* mm, char *start, char *end) { ERTS_MMAP_ASSERT(end == (void *) 0 || end > start); if (sizeof(ErtsFreeSegDesc) <= ((UWord) end) - ((UWord) start)) { .... } .... } void erts_mmap_init(ErtsMemMapper* mm, ErtsMMapInit *init) { .... if (end == (void *) 0) { /* * Very unlikely, but we need a guarantee * that `mm->sua.top` always will * compare as larger than all segment pointers * into the super carrier... */ mm->sua.top -= ERTS_PAGEALIGNED_SIZE; mm->size.supercarrier.used.total += ERTS_PAGEALIGNED_SIZE; #ifdef ERTS_HAVE_OS_PHYSICAL_MEMORY_RESERVATION if (!virtual_map || os_reserve_physical(mm->sua.top, ERTS_PAGEALIGNED_SIZE)) #endif add_free_desc_area(mm, mm->sua.top, end); mm->desc.reserved += (end - mm->sua.top) / sizeof(ErtsFreeSegDesc); } .... } Enter fullscreen mode Exit fullscreen mode アナリストは疑わしいアドレス数値を検出しました. ここで何が間違っているか見てみましょう. We enter the code branch where is indeed a null pointer. The data member is also a pointer, but we don't know its value yet. According to the C standard, the behavior of pointers that differ is defined only when they both point to elements of the same array (C11)。 C23 ) null ポインターは、いかなる数列要素にも指さないため、この操作の動作は未定義で、これは良いものではありません。 end mm->sua.top 6.5.6.8 5.7 10 I was wondering what happens in the 機能です。 add_free_desc_area static void add_free_desc_area(ErtsMemMapper* mm, char *start, char *end) { ERTS_MMAP_ASSERT(end == (void *) 0 || end > start); if (sizeof(ErtsFreeSegDesc) <= ((UWord) end) - ((UWord) start)) { .... } Enter fullscreen mode Exit fullscreen mode Pointer subtraction is also used here, but with a difference: first, the pointers are converted to numbers. This is implementation-defined behavior (C11 ; C23 )が、はるかに優れている。 6.3.2.3.6 6.3.2 3.6 Unfortunately, I can't suggest a proper fix. The developers might want to take a look into this issue. 結論 Well, that concludes the article. As expected with any large project that has had a long and eventful history, we found various errors. They're also part of the project, though. It's important to keep in mind that finding these kinds of issues in a code base that's over three decades old isn't a sign of weakness. Rather, it's proof of the code's incredible scale and longevity. There are millions of code lines written by hundreds of developers, and the system has been running for years without rebooting. This project deserves the deepest respect, not because it's perfect, but because it has stood the test of time and proven its real power. 継続的に進化する近代技術のおかげで、開発者はコードの信頼性を向上させるための強力なツールを持っています。例えば、静的分析器は隠れたエラーと潜在的な脆弱性を特定するのに役立ちます。 . here