未定義動作への関心を呼び起こした、本番環境で発生したバグ。

2025/12/30 3:17

未定義動作への関心を呼び起こした、本番環境で発生したバグ。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

改訂版まとめ

この記事では、C++ の HTTP‑API ハンドラにおける実際のバグを報告しています。

Response
構造体は 2 つの
bool
メンバーと 1 つの
std::string
を持ち、デフォルト構築された場合です。この構造体には非 POD(Plain Old Data)メンバーがあるため、コンパイラは各フィールドを デフォルト初期化 するだけのデフォルトコンストラクタを生成します。結果として
bool
フィールドは未定義状態のままとなり、これらの値を読み取ると
{ "error": true, "succeeded": true }
が返されますが、実際にはエラーは発生していません。

記事では C++ のデフォルト初期化規則について説明し、2 つの修正策を提示しています。

  1. 明示的に両方の
    bool
    メンバーを
    false
    に初期化するデフォルトコンストラクタを追加(文字列は
    {}
    を使用)。
  2. クラス内でデフォルト値を設定 (
    bool error = false; bool succeeded = false;
    )。

簡易的な回避策として、変数を値初期化 (

Response response{};
) するとすべてのフィールドがゼロパディングされ、定義を変更せずに問題を解消できます。

また、記事は静的解析ツールについてもレビューしています。Clang/Tidy は構造体が関数へ渡された際にこのパターンを検出でき、cppcheck も過去には検出していましたが、将来の回帰が懸念されています。実行時サニタイザー(ASan/UBSan)は無効な

bool
値 (
8
) を検出することで未定義動作をフラグします。

同様のケースをビルド時に捕捉するため、著者は libclang プラグインを構築しました。コードベースで 1 件のみ偽陽性が検出されました。記事では

bool
の未定義値読取は UB とされていますが、
unsigned char
std::byte
のような型ではそうではないと指摘しています。

結論として、構造体設計と初期化に細心の注意を払うことで、予測不能な API 応答や下流での失敗につながる微妙な未定義動作を回避するよう促しています。

本文

すべての記事へ戻る

公開日: 2025‑12‑27


目次

  1. バグレポート
  2. 調査
  3. 自己処理に十分なロープ
  4. 結果
  5. 静的解析が救う?
  6. 実行時解析で救われる
  7. 再びの結果
  8. あなたへ贈るC++ルール集
  9. まとめ
  10. 補足

1. バグレポート

ひとつのHTTPエンドポイントが、成功か失敗を示す小さなJSON‑ishペイロードを返します。

{ "error": false, "succeeded": true }

あるいは

{ "error": true,  "succeeded": false }

(実際のフォーマットは form‑encoded だったかもしれませんが、バグ自体はそれとは無関係です)

クライアントからは以下のようなレスポンスを受け取ったと報告されました。

{ "error": true, "succeeded": true }

これはあり得ない状態です。


2. 調査

該当コードはひとつの関数に集約されています。

struct Response {
    bool error;
    bool succeeded;
    std::string data;
};

void handle() {
    Response response;

    try {
        // … データベース処理(response には触れない)
        response.succeeded = true;
    } catch (...) {
        response.error = true;
    }
    response.write();
}

構造体のフィールドを書き換える場所は二箇所だけです。
それでも両方のブールが

true
になることがどうして起こるのでしょうか?


3. 自己処理に十分なロープ

原因は 初期化されていないメンバー にありました。

C++ のデフォルト初期化規則(簡略版)

宣言結果
プリミティブ (
int
,
bool
)
int x;
未定義値 (indeterminate)
POD / トリアル構造体
Foo f;
すべてのフィールドが未定義
配列
std::string s[10];
各要素はデフォルト初期化される
非トリビアル構造体(非トリビアルメンバーを持つ)
Response r;
デフォルトコンストラクタが呼ばれ、プリミティブメンバーは未定義のまま

Response
std::string
を含むため、コンパイラはデフォルトコンストラクタを生成し
data{}
を呼びます。
しかし
bool
メンバーは 未定義 のままで、読み取ると未定義動作(UB)になります。


4. 結果

簡単な修正策

  1. 明示的にデフォルトコンストラクタを定義

    struct Response {
        bool error;
        bool succeeded;
        std::string data;
    
        Response() : error(false), succeeded(false), data{} {}
    };
    
  2. クラス内イニシャライザを使う

    struct Response {
        bool error = false;
        bool succeeded = false;
        std::string data;
    };
    
  3. 呼び出し側でゼロ初期化

    Response response{};  // すべてのメンバーを値初期化
    

後者は構造体定義を変更せずに済むため、最も簡単です。


5. 静的解析が救う?

  • clang
    単独では
    -Weverything
    を付けても警告しません。
  • clang-tidy
    は検出できることがありますが、従来はその変数を関数に渡したときだけ報告していました。
  • cppcheck
    はバージョンやオプションによって見逃す場合があります。

6. 実行時解析で救われる

AddressSanitizer (ASan) を使うと UB が検出されます。

clang++ -std=c++11 -g -fsanitize=address,undefined main.cpp && ./a.out

実行結果では無効な

bool
のロードがハイライトされます。
Valgrind でも同様に検出可能です。

これらツールは非常に有用ですが、実行時オーバーヘッドが発生し、一部ケースを見逃すことがあります。


7. 再びの結果

ビルド時にこのような問題を捕捉するために、独自の libclang プラグインを作成しました。
コードベースでは他に誤検知は1件だけでした。


8. あなたへ贈るC++ルール集

いくつかの型は 特殊 です:

未初期化時の挙動
std::byte
,
unsigned char
,
char
(符号なし)
コピー/代入で UB は起きず、値は未定義のまま読み取れる。
bool
書き込む前に読めば UB。ASan で即座に検出されます。

例:

unsigned char c;           // 未定義
unsigned char d = c;       // OK、d は未定義のまま
assert(c == d);            // 成り立つが両方未定義

bool e;
bool f = e;                // UB – ASan がフラグを立てる

9. まとめ

  • C で無害だった構文も、C++ では危険になることがあります。
  • デフォルト初期化規則は微妙でバージョン依存です。
  • 未定義動作はプログラム状態を暗黙に破壊し、信頼性を損ないます。
  • 静的ツールは限界があり、実行時サニタイザーがほとんどの問題を検出しますが、性能への影響があります。

未初期化の

bool
が本番環境で実際にバグを引き起こしました。
これらのニュアンスを理解し、安全な C++ 開発を行うことが不可欠です。


10. 補足

フォレスト・ガンプ GIF を忘れていたので、コメントしてくださった方に感謝します!

同じ日のほかのニュース

一覧に戻る →

2025/12/30 6:46

USPS(米国郵便公社)が切手印日付システムの変更を発表しました。

## Japanese Translation: > **概要:** > USPSは最終規則(FR Doc. 2025‑20740)を発行し、国内郵便マニュアルに「セクション 608.11 —『切手印と郵便保有』」を追加しました。この規則では、切手印の定義が正式に示され、該当する印記がリストアップされています。切手印は印付け日でUSPSがその物件を保有していることを確認しますが、必ずしもアイテムの最初の受理日と同一ではありません。USPSは通常業務で全ての郵便に切手印を貼らないため、切手印が欠落していても、その物件が未処理だったとは限りません。機械による自動切手印は、施設内で最初に行われた自動処理操作の日付(「date of the first automated processing operation」)を表示し、投函日ではなく、地域輸送最適化(RTO)や路線ベースのサービス基準により受理日より遅くなることがあります。切手印は小売ユニットからの輸送後やカレンダー日がまたがる場合に付けられることが多いため、郵送日を示す信頼できる指標ではありません。同一日の切手印を確保するには、小売窓口で手動(ローカル)切手印を依頼できます。小売窓口で料金を支払うと「Postage Validation Imprint(PVI)」が付与され、受理日が記録されます。また、郵便証明書、登録メール、または認定メールは提示日を裏付ける領収書として機能します。この規則の影響は税務申告において重要です。IRC §7502 は、文書が期限までに物理的に届けられなかった場合に、提出の適時性を判断する際に切手印の日付を使用しています。

2025/12/30 1:07

**Zig における静的割り当て** Zig のコンパイル時メモリ管理を使えば、実行時ではなくコンパイル時にストレージを確保できます。データ構造のサイズが事前に分かっている場合やヒープ割り当てを避けたいときに便利です。 ### 重要概念 - **コンパイル時定数** `const` や `comptime` の値を使い、コンパイラがコンパイル中に評価できるサイズを記述します。 - **固定長配列** リテラルサイズで配列を宣言します。 ```zig const buf = [_]u8{0} ** 128; // 128 バイト、すべてゼロ初期化 ``` - **静的フィールドを持つ構造体** 固定長配列やその他コンパイル時に決まる型を含む構造体を定義します。 ### 例 ```zig const std = @import("std"); // 静的サイズのバッファを持つ構造体 pub const Message = struct { id: u32, payload: [256]u8, // 256 バイト、コンパイル時に確保 }; // 静的割り当てを使う関数 fn process(msg: *Message) void { // ヒープ割り当ては不要;msg はスタック上またはグローバルに存在 std.debug.print("ID: {d}\n", .{msg.id}); } pub fn main() !void { var msg = Message{ .id = 42, .payload = [_]u8{0} ** 256, // すべてのバイトをゼロで初期化 }; process(&msg); } ``` ### 利点 - **決定的なメモリ使用量** – サイズはコンパイル時に分かる - **実行時割り当てオーバーヘッドがゼロ** – ヒープアロケータ呼び出しなし - **安全性** – コンパイラが境界と寿命を検証できる ### 使うべき場面 - 固定長バッファ(例:ネットワークパケット、ファイルヘッダー) - 短時間しか存続しない小規模補助データ構造 - 性能や決定的な動作が重要な状況 --- コンパイル時定数・固定配列・構造体定義を活用することで、Zig は最小限のボイラープレートで最大の安全性を保ちつつメモリを静的に割り当てることができます。

## Japanese Translation: > **概要:** > このプロジェクトは、Zigで書かれた軽量Redis互換のキー/バリューサーバー「kv」を構築し、最小限のコマンドセットで本番環境に適した設計を目指しています。コアデザインでは起動時にすべてのメモリを確保することで、実行中にダイナミックヒープを使用せず、レイテンシスパイクやユース・アフター・フリー(use‑after‑free)バグを回避します。接続は`io_uring`で非同期に処理され、システムは3つのプール(Connection、受信バッファプール、送信バッファプール)を事前確保し、デフォルトでは約1000件までの同時接続数をサポートします。各接続は設定パラメータから派生した固定サイズの受信/送信バッファを使用します。 > コマンド解析はRedisのRESPプロトコルのサブセットに従い、Zigの`std.heap.FixedBufferAllocator`を用いてゼロコピーで解析し、各リクエスト後にアロケータをリセットします。バッファサイズは`list_length_max`と`val_size_max`に依存します。 > ストレージは未管理型の`StringHashMapUnmanaged(Value)`を使用し、初期化時に`ensureTotalCapacity`で容量を確保します。キーと値は共有`ByteArrayPool`に格納され、マップはポインタのみを保持します。削除操作では墓石(tombstone)が残り、墓石数が増えると再ハッシュが必要になる場合があります。 > 設定構造体(`Config`)は `connections_max`、`key_count`、`key_size_max`、`val_size_max`、`list_length_max` などのフィールドを公開し、派生アロケーションで接続ごとのバッファサイズを決定します。デフォルト設定(総計約748 MB、2048エントリ)では `val_size_max` または `list_length_max` を倍増すると、割り当て量が約2.8 GBに上昇する可能性があります。 > 今後の作業としては、カスタム静的コンテキストマップ実装の改善、より良いメモリ利用を実現する代替アロケータの探索、境界検査(fuzz)テストの追加による限界確認、および墓石再ハッシュ処理への対応が挙げられます。

2025/12/27 20:30

**フレームグラフ 対 ツリーマップ 対 サンバースト(2017)**

## Japanese Translation: **概要:** Flame グラフ(SVG)はディスク使用量を高レベルで明確に示します。たとえば、Linux 4.9‑rc5 では `drivers` ディレクトリが全容量の50%以上を占め、`drivers/net` サブディレクトリは約15%です。Tree マップ(macOS の GrandPerspective、Linux の Baobab)は非常に大きなファイルを素早く検出できますが、高レベルのラベルが欠けています;Baobab のツリー表示では各ディレクトリの横にミニバーグラフが表示されます。Sunburst(Baobab の極座標図)は視覚的に印象的ですが、角度で大きさを判断するため長さや面積よりも誤解しやすいです。他のツール―`ncdu` の ASCII バーと `du -hs * | sort -hr` ―はテキストベースで迅速なサマリーを提供しますが、同時に一階層のみ表示されます。 提案されたユーティリティは、これら三つの可視化(Flame グラフ(デフォルト)、Tree マップ、Sunburst)すべてを組み合わせるものです。Flame グラフは読みやすさ・印刷性・最小スペース使用量が優れているため、多数のサンプルファイルシステムでテストした後にデフォルトとして採用されます。このアプローチは、ディスク使用量を簡潔かつ印刷可能なスナップショットとして提供し、ユーザーや開発者がスペースを占有する項目をより効率的に検出できるよう支援します。アイデアは ACMQ の「The Flame Graph」記事と「A Tour through the Visualization Zoo」に引用された既存の研究に基づいています。 **反映された主なポイント:** flame グラフの高レベルビュー、Tree マップの大きなファイルを素早く検出できるがラベルが欠けている点、Sunburst の視覚的魅力とサイズ認識の問題、他ツールの制限、および提案ツールの三つのビュー(デフォルトは flame グラフ)と引用元への参照。