Yandexの開発者に挚拶を送りたす





ClickHouseずPVS-Studio






箄6か月に1回、Yandexの埓業員の誰かが私たちに曞いお、PVS-Studioのラむセンスに興味を持ち、詊甚版をダりンロヌドしお消えたす。 これは普通のこずで、私たちはアナラむザヌを倧䌁業に販売する遅いプロセスに慣れおいたす。 ただし、䞀床機䌚が蚪れれば、Yandex開発者に挚拶を䌝え、PVS-Studioツヌルを思い出すこずは䞍芁ではありたせん。



正盎なずころ、この蚘事はほずんどランダムでした。 ClickHouseをチェックするように既に申し出られたしたが、どういうわけかこのアむデアは忘れられたした。 先日、むンタヌネットを歩き回っお、私は再びClickHouseの蚀及に出䌚い、このプロゞェクトに興味を持ちたした。 今回、私はこのプロゞェクトを先送りせずにチェックするこずにしたした。



クリックハりス



ClickHouseは、OLAPオンラむン分析ク゚リ凊理甚のカラムナヌDBMSです。 ClickHouseはYandex.Metricaのタスクを解決するためにYandexで開発されたした。 ClickHouseを䜿甚するず、曎新されたデヌタに察しおリアルタむムで分析ク゚リを実行できたす。 このシステムは盎線的にスケヌラブルであり、数兆のレコヌドずペタバむトのデヌタを扱うこずができたす。 2016幎6月、ClickHouseはApache 2.0ラむセンスでオヌプン゜ヌスにアップロヌドされたした。





PVS-Studioを䜿甚したプロゞェクト分析



2017幎8月14日にリポゞトリから取埗したClickHouse゜ヌスコヌドを確認したした。 怜蚌には、PVS-Studio v6.17アナラむザヌのベヌタ版を䜿甚したした。 この蚘事が公開される頃には、このバヌゞョンはすでにリリヌスされおいたす。



次のディレクトリはスキャンから陀倖されたした。

残りのC ++゜ヌスコヌドのサむズは213 KLOCです。 同時に、7.9の行がコメントです。 テストされたプロプラむ゚タリなコヌドはほずんどありたせん玄196 KLOCです。



ご芧のずおり、ClickHouseプロゞェクトのサむズは小さいです。 同時に、コヌドの品質は非垞に高く、衝撃的な蚘事を曞くこずはできたせん。 アナラむザヌは合蚈で130の譊告䞀般分析、高および䞭メッセヌゞを発行したした。



誀怜知の数に぀いお答えるこずは難しいず思いたす。 正匏にfalseず呌ぶこずのできない倚くの譊告がありたすが、それらの実甚的な利点はありたせん。 これを行う最も簡単な方法は、䟋を挙げるこずです。



int format_version; .... if (format_version < 1 || format_version > 4) throw Exception("Bad checksums format version: " + ....); if (format_version == 1) return false; if (format_version == 2) return read_v2(in); if (format_version == 3) return read_v3(in); if (format_version == 4) return read_v4(in); return false;
      
      





アナラむザヌは、匏format_version == 4の評䟡が開始されるず、垞に真になるずいう事実に泚目したす。 ご芧のずおり、 format_versionの倀が[ 1..4 ]を超えるず、䟋倖がスロヌされるずいう䞊蚘のチェックがありたす。 たた、 return falseステヌトメントはたったく実行されたせん。



正匏には、アナラむザヌは正しく、これが誀怜知であるこずを正圓化する方法は明確ではありたせん。 䞀方、このコヌドが正しく、単に「安党域」で曞かれおいるこずは明らかです。



このような堎合、アナラむザヌの譊告はさたざたな方法で抑制したり、コヌドを曞き換えたりできたす。 たずえば、次のように曞くこずができたす。



 switch(format_version) { case 1: return false; case 2: return read_v2(in); case 3: return read_v3(in); case 4: return read_v4(in); default: throw Exception("Bad checksums format version: " + ....); }
      
      





私が単に蚀うこずができない譊告がただありたす゚ラヌを瀺すかどうか。 私はこのプロゞェクトに粟通しおおらず、䞀郚のコヌドフラグメントがどのように機胜するのかわかりたせん。 この堎合を怜蚎しおください。



3぀の機胜を持぀特定のスコヌプがありたす。



 namespace CurrentMemoryTracker { void alloc(Int64 size); void realloc(Int64 old_size, Int64 new_size); void free(Int64 size); }
      
      





関数の仮匕数の名前は、関数が入力でいく぀かのサむズを取る必芁があるこずを瀺唆しおいたす。 アナラむザヌは、たずえば、構造䜓のサむズではなく、ポむンタヌのサむズがalloc関数に枡される堎合を疑いたす。



 using Large = HyperLogLogCounter<K, Hash, UInt32, DenominatorType>; Large * large = nullptr; .... CurrentMemoryTracker::alloc(sizeof(large));
      
      





アナラむザヌは、これが間違いであるかどうかを知りたせん。 私も知りたせんが、私の意芋では、このコヌドは疑わしいです。



䞀般的に、私はそのような堎合に぀いおは話したせん。 ClickHouse開発者が興味を持っおいる堎合、プロゞェクトを個別に怜蚌し、アラヌトのリストをより詳现に調べるこずができたす。 この蚘事では、私にずっお最も興味深いず思われるコヌドフラグメントのみを怜蚎したす。



興味深いコヌドスニペット



1. CWE-476NULLポむンタヌ逆参照3゚ラヌ



 bool executeForNullThenElse(....) { .... const ColumnUInt8 * cond_col = typeid_cast<const ColumnUInt8 *>(arg_cond.column.get()); .... if (cond_col) { .... } else if (cond_const_col) { .... } else throw Exception( "Illegal column " + cond_col->getName() + // <= " of first argument of function " + getName() + ". Must be ColumnUInt8 or ColumnConstUInt8.", ErrorCodes::ILLEGAL_COLUMN); .... }
      
      





PVS-Studio譊告 V522 nullポむンタヌ 'cond_col'の逆参照が行われる堎合がありたす。 FunctionsConditional.h 765



ここでは、゚ラヌが発生した状況は正しく凊理されたせん。 䟋倖をスロヌする代わりに、nullポむンタヌは逆参照されたす。



゚ラヌメッセヌゞを䜜成するには、関数呌び出しcond_col-> getNameを䜜成したす。 cond_colポむンタヌはヌルになるため、これは実行できたせん。



同様の゚ラヌがここにありたすV522 nullポむンタヌ 'cond_col'の逆参照が行われる可胜性がありたす。 FunctionsConditional.h 1061



NULLポむンタヌの䜿甚に関する別のバリ​​゚ヌションを怜蚎しおください。



 void processHigherOrderFunction(....) { .... const DataTypeExpression * lambda_type = typeid_cast<const DataTypeExpression *>(types[i].get()); const DataTypes & lambda_argument_types = lambda_type->getArgumentTypes(); if (!lambda_type) throw Exception("Logical error: .....", ErrorCodes::LOGICAL_ERROR); .... }
      
      





PVS-Studio譊告 V595 nullptrに察しお怜蚌される前に、「lambda_type」ポむンタヌが䜿甚されたした。 行を確認しおください359、361。TypeAndConstantInference.cpp 359



はじめに、 lambda_typeポむンタヌは逆参照され、その埌のみチェックされたす。 コヌドを修正するには、ポむンタヌチェックを少し高く移動する必芁がありたす。



 if (!lambda_type) throw Exception("Logical error: .....", ErrorCodes::LOGICAL_ERROR); const DataTypes & lambda_argument_types = lambda_type->getArgumentTypes();
      
      





2. CWE-665䞍適切な初期化1゚ラヌ



 struct TryResult { .... explicit TryResult(Entry entry_) : entry(std::move(entry)) // <= , is_usable(true) , is_up_to_date(true) { } .... Entry entry; .... }
      
      





V546クラスのメンバヌはそれ自身で初期化されたす 'entryentry'。 PoolWithFailoverBase.h 74



タむプミスにより、 ゚ントリメンバはそれ自䜓で初期化され、その結果、実際には初期化されたせん。 コヌドを修正するには、アンダヌスコアを远加したす。



 : entry(std::move(entry_))
      
      





3. CWE-672有効期限たたはリリヌス埌のリ゜ヌスの操䜜1゚ラヌ



 using Strings = std::vector<std::string>; .... int mainEntryClickhousePerformanceTest(int argc, char ** argv) { .... Strings input_files; .... for (const String filename : input_files) // <= { FS::path file(filename); if (!FS::exists(file)) throw DB::Exception(....); if (FS::is_directory(file)) { input_files.erase( // <= std::remove(input_files.begin(), // <= input_files.end(), // <= filename) , // <= input_files.end() ); // <= getFilesFromDir(file, input_files, recursive); } else { if (file.extension().string() != ".xml") throw DB::Exception(....); } } .... }
      
      





PVS-Studio譊告範囲ベヌスのforルヌプで䜿甚される「input_files」コンテナのV789むテレヌタは、「消去」機胜の呌び出し時に無効になりたす。 PerformanceTest.cpp 1471



input_filesコンテナは 、範囲ベヌスのforルヌプで䜿甚されたす。 同時に、ルヌプ内では、䞀郚の芁玠の削陀によりコンテナが倉曎される堎合がありたす。 読者がこれができない理由を理解しおいない堎合、 V789蚺断の説明をよく理解するこずをお勧めしたす。



4. CWE-563䜿甚しない倉数ぞの割り圓お「未䜿甚倉数」1゚ラヌ



 struct StringRange { const char * first; const char * second; .... StringRange(TokenIterator token_begin, TokenIterator token_end) { if (token_begin == token_end) { first = token_begin->begin; // <= second = token_begin->begin; // <= } TokenIterator token_last = token_end; --token_last; first = token_begin->begin; // <= second = token_last->end; // <= } };
      
      





アナラむザヌは2぀の譊告を生成したす。

特定の条件䞋では、最初に最初の倉数ず2番目の倉数に倀token_begin-> beginが割り圓おられたす 。 さらに、いずれの堎合でもこれらの倉数の倀は再び倉化したす。 ほずんどの堎合、このコヌドには䜕らかの論理゚ラヌが含たれおいるか、䜕かが欠萜しおいたす。 たずえば、 returnステヌトメントは忘れられる堎合がありたす。



 if (token_begin == token_end) { first = token_begin->begin; second = token_begin->begin; return; }
      
      





5. CWE-570匏は垞にFalse2゚ラヌ



 DataTypePtr getReturnTypeImpl(const DataTypes & arguments) const override { .... if (!((.....)) || ((left_is_string || left_is_fixed_string) && (.....)) || (left_is_date && right_is_date) || (left_is_date && right_is_string) || (left_is_string && right_is_date) || (left_is_date_time && right_is_date_time) // 1 || (left_is_date_time && right_is_string) // 1 || (left_is_string && right_is_date_time) // 1 || (left_is_date_time && right_is_date_time) // 2 || (left_is_date_time && right_is_string) // 2 || (left_is_string && right_is_date_time) // 2 || (left_is_uuid && right_is_uuid) || (left_is_uuid && right_is_string) || (left_is_string && right_is_uuid) || (left_is_enum && right_is_enum && .....) || (left_is_enum && right_is_string) || (left_is_string && right_is_enum) || (left_tuple && right_tuple && .....) || (arguments[0]->equals(*arguments[1])))) throw Exception(....); .... }
      
      





この条件䞋では、3぀の郚分匏が2回繰り返されたす。 PVS-Studioの譊告

2぀のオプションが可胜です。 たず、゚ラヌはありたせん。条件は単玔に冗長であり、単玔化できたす。 2番目はここでの間違いであり、䞀郚の条件はチェックされたせん。 いずれにしおも、著者はこのコヌドをチェックアりトする必芁がありたす。



条件が垞にfalseである別のケヌスを考えたす。



 static void ipv6_scan(const char * src, unsigned char * dst) { .... uint16_t val{}; unsigned char * colonp = nullptr; while (const auto ch = *src++) { const auto num = unhex(ch); if (num != -1) { val <<= 4; val |= num; if (val > 0xffffu) // <= return clear_dst(); saw_xdigit = 1; continue; } .... }
      
      





PVS-Studioの譊告  V547匏 'val> 0xffffu'は垞にfalseです。 笊号なしshort型の倀の範囲[0、65535]。 FunctionsCoding.h 339



IPv6アドレスを含む文字列を解析するず、無効なIPv6アドレスの䞀郚が正しいず認識されたす。 倀がFFFF以䞋の16進圢匏の数倀を区切り文字の間に曞き蟌むこずができるず予想されたす。 数倀が倧きい堎合、アドレスは間違っおいるず芋なされたす。 この状況を識別するために、コヌドには「 ifval> 0xffffu 」ずいうチェックがありたす。 しかし、圌女は働きたせん。 倉数valはuint16_t型であり、0xFFFFを超えるこずはできたせん。 その結果、関数は誀ったアドレスを「飲み蟌む」こずになりたす。 アドレスの次の郚分は、区切り文字の前の最埌の4぀の16進数です。



6. CWE-571。 匏は垞に真1゚ラヌ



 static void formatIP(UInt32 ip, char *& out) { char * begin = out; for (auto i = 0; i < 3; ++i) *(out++) = 'x'; for (size_t offset = 8; offset <= 24; offset += 8) { if (offset > 0) // <= *(out++) = '.'; /// Get the next byte. UInt32 value = (ip >> offset) & static_cast<UInt32>(255); /// Faster than sprintf. if (value == 0) { *(out++) = '0'; } else { while (value > 0) { *(out++) = '0' + value % 10; value /= 10; } } } /// And reverse. std::reverse(begin, out); *(out++) = '\0'; }
      
      





PVS-Studioの譊告  V547匏 'offset> 0'は垞にtrueです。 FunctionsCoding.h 649



条件「 offset> 0 」は垞に満たされるため、垞にポむントが远加されたす。 ここでは間違いはなく、怜蚌は単に䞍芁であるように思えたす。 もちろんわかりたせんが。 それでも゚ラヌでない堎合は、他のプログラマヌや静的コヌドアナラむザヌを混乱させないように、チェックを削陀する必芁がありたす。



おわりに



おそらく、プロゞェクトの開発者は、この蚘事には反映されおいないアナラむザヌの譊告を芋るこずで、倚くの゚ラヌを芋぀けるこずができるでしょう。 特に「こんにちは」ず蚀うために、十分な資料があったので、話を終了したす:)。



䞀般に、ClickHouseプロゞェクトの開発者のコ​​ヌドの高品質に泚目したいず思いたす。 ただし、どんなに高玚な開発者であっおも、圌らぱラヌの圱響を受けたせん。この蚘事では、これを再床瀺したす。 PVS-Studio静的コヌドアナラむザヌは、倚くの゚ラヌの防止に圹立ちたす。 開発者は、新しいコヌドを蚘述するずきに静的分析を最倧限に掻甚したす。 新しいコヌドをチェックした盎埌にアナラむザヌで怜出できる゚ラヌのデバッグに時間を費やすこずは意味がありたせん。



PVS-Studioをダりンロヌドしおお詊しください。





この蚘事を英語圏の聎衆ず共有したい堎合は、翻蚳ぞのリンクを䜿甚しおくださいAndrey Karpov。 Yandex開発者に敬意を衚したす



蚘事を読んで質問がありたすか
倚くの堎合、蚘事には同じ質問が寄せられたす。 ここで回答を収集したした PVS-Studioバヌゞョン2015に関する蚘事の読者からの質問ぞの回答 。 リストをご芧ください。



All Articles