1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

fdリークの見落としは「早期return」に集中する、7件の内訳から言えること

1
Posted at

C言語23年、自分はフリーランスで組み込みのコードレビューに入ることが多いです。fd・socket・FILEのclose忘れは静的解析の定番テーマですが、実際のPRで見逃されるのは「close自体を書き忘れた行」ではなく「正常系ではcloseしているのに、途中のreturnでcloseし損ねている行」です。今回検証したコードは4関数・37行程度の小さなものですが、creviewは7件を検出し、そのうち3件がリソースリーク、4件がNULL契約未表明でした。件数の内訳を見ると、レビューワが読み飛ばしやすい箇所がどこかが逆算できます。

SCQA

  • 状況: fd/socket/FILEのclose忘れは、CWE-401としてどの静的解析ツールの資料にも載っている定番の指摘項目です。
  • 問題: 定番だからといって手動レビューで拾えるとは限りません。特に「異常系のreturnパス」は正常系のロジックを追うのに気を取られて読み飛ばされやすく、今回のread_first_byte()のように、正常系ではcloseしているのに異常系のreturnではcloseし忘れる、というパターンが典型です。
  • 問い: 37行程度の小さな関数群でも7件の指摘が出るということは、レビューワは何を根拠に「ここは大丈夫」と判断してしまっているのか。
  • 答え: creviewでopen/socket/fopen/pipeそれぞれの戻り値に対して、全returnパスでのclose有無を機械的に突き合わせ、人間は「そのリソースを本当にここで手放してよいか」という設計判断だけに集中する分業にしました。

L0: まずコンパイラ警告を全部潰す

creviewの話に入る前に、-Wall -Wextra -Wpedantic -Werror -fanalyzer(GCC)または-Wall -Wextra -Werror -Weverything(Clang)を有効にしてビルドが通るか確認してください。NULL引数をnonnull関数に渡す・未初期化auto変数を読む・戻り値を破棄する・自明なNULL deref・use-after-free・境界外配列アクセス(静的決定可なもの)・switch case抜け、これらは現代のコンパイラが標準で拾います。

今回の検証コードでいえば、-fanalyzerを付けたGCCはfile_size()のfclose忘れパスの一部を検出できる可能性があります。ただしopen/socket/pipeのfdリークは処理系依存で拾えたり拾えなかったりするため、これらが警告ゼロになった前提の上で、creviewのリソースリーク検出を次の砦として使うのが現実的な順番です。警告ゼロにならないPRは、creviewを走らせる前にそもそもレビュー対象にしないルールにしてしまうのが第一手です。

検証用コード

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>

/* fd を open して close せずに早期 return */
int read_first_byte(const char *path) {
    int fd = open(path, O_RDONLY);       /* 検出すべき: close なし */
    if (fd < 0) return -1;
    char b;
    if (read(fd, &b, 1) != 1) {
        return -1;                       /* 検出すべき: ここで close 漏れ */
    }
    close(fd);
    return (int)b;
}

/* socket を作って close せずに return */
int connect_to(const char *host) {
    int s = socket(AF_INET, SOCK_STREAM, 0);  /* 検出すべき: close なし */
    (void)host;
    return s;
}

/* fopen と FILE のリーク */
long file_size(const char *path) {
    FILE *fp = fopen(path, "rb");        /* 検出すべき: fclose なし */
    if (!fp) return -1;
    fseek(fp, 0, SEEK_END);
    return ftell(fp);                    /* fclose 漏れ */
}

/* pipe の片側だけ close */
void make_channel(int *r, int *w) {
    int p[2];
    pipe(p);                             /* 検出すべき: 両端 close 漏れの典型 */
    *r = p[0];
    /* p[1] を返さずに捨てる → fd リーク */
}

read_first_byte()は一見「ちゃんとcloseしている関数」に見えます。18行目でclose(fd)を書いているからです。しかし12〜14行目のread()失敗パスにはcloseが無く、正常系だけ見て「このコードは大丈夫」と判断すると通り抜けます。手動レビューでこの手のパターンを見落としやすいのは、まさに「一度closeを書いた実績があるコード」を後半だけ読んで安心してしまう時です。

CWE/MISRA マッピング

検出パターン 該当行 CWE 事故の形
open()後、read失敗時にclose無しでreturn L8-14 CWE-401 異常系を通るたびにfdを1つ消費、長時間稼働プロセスでfd枯渇
socket()後、closeせずreturn L20-21 CWE-401 接続確立の成否に関わらずfdリーク、サーバプロセスでは早期に上限到達
fopen()後、fclose無しでreturn L27-28 CWE-401 FILE構造体とfdの二重リーク、ファイルサイズ取得のような軽い処理でも蓄積する
pipe()の書き込み側を呼び出し元に返さず破棄 L35-37 CWE-401 読み出し側だけ返す設計自体が両端管理を難しくし、書き込み側が永久にリークする
path/host/r のNULL未検査使用 L8, L20, L27, L35 CWE-476 外部境界からNULLが渡ると即座にクラッシュ、契約が関数シグネチャに現れていない

read_first_byte()のCWE-401は、open()直後の単純な閉じ忘れではなく「returnパスが複数あるうちの1本だけ閉じ忘れている」点が特徴です。この形はコードレビューでのdiff読みでは、変更差分に異常系のreturn文が含まれていないと見えにくく、既存コードの手直し時に見落としが起きやすい典型例です。

creview の使い方(CLI 実行例)

creview review resource_leak_sample.c --preset security --format markdown
L8 【重大】: ATTR-NONNULL-001: read_first_byte() の path を NULL 検査せず使用。__attribute__((nonnull(N))) で契約を明示するか、関数先頭で NULL 検査して return
L20 【重大】: ATTR-NONNULL-001: connect_to() の host を NULL 検査せず使用。__attribute__((nonnull(N))) で契約を明示するか、関数先頭で NULL 検査して return
L21 【重大】: socket結果sに対応するcloseなし。ソケットリーク
L27 【重大】: ATTR-NONNULL-001: file_size() の path を NULL 検査せず使用。__attribute__((nonnull(N))) で契約を明示するか、関数先頭で NULL 検査して return
L28 【重大】: fopen結果fpに対応するfcloseなし。ファイルディスクリプタリーク
L35 【重大】: ATTR-NONNULL-001: make_channel() の r を NULL 検査せず使用。__attribute__((nonnull(N))) で契約を明示するか、関数先頭で NULL 検査して return
L37 【重大】: pipe(p)に対応するcloseなし。fdリーク

3段階ラベルの読み方は以下の通りです。

  • 【重大】: 実害に直結する、または契約未表明でクラッシュや資源枯渇に至る指摘。今回の7件は全てこのラベルで、resource leak系4件とnonnull契約未表明系4件が該当します(connect_to()はsocketリークとhostのnonnull両方でL20に集約)。
  • 【設計不明】: 今回の検出結果には出ていませんが、意図的な設計かバグか判断がつかない箇所(例えばエラーコードを握りつぶすswitch文など)に付くラベルです。
  • 【保守危険】: 今すぐ落ちはしないが、将来の変更で壊れやすい書き方(マジックナンバー、暗黙の型変換など)に付くラベルです。

今回の7件が全て【重大】に振られている点は、材料としてそのまま受け取っておくべきです。「重大」が並ぶこと自体が、この4関数がレビュー観点では要注意コードだという裏付けになります。

pipe周りのL37の指摘は、pipe(p)という1行に対する指摘でありながら、実際に見るべきは*r = p[0]だけ代入してp[1]を握り潰している34〜37行目の設計です。creviewの指摘行と、人間が読むべき行がずれることがあるため、行番号だけでなく前後の文脈まで読む必要があります。

3段階の使い分け

場面 使うツール 使いどころ
L0: ビルド時(まずここ) gcc / clangの警告 + -Werror -Wall -Wextra -Wpedantic -Werror -fanalyzer(GCC)/ -Weverything -Werror(Clang)。警告ゼロにならないPRはそもそもレビューに入らないルール化
L1: PRチェック clang-tidy / cppcheck OSSの主流チェックをCIで強制
L2: PRレビュー creview(CLI、自社プロダクト) プロジェクト固有パターン、日本語ナレッジ、severityモデル(重大/設計不明/保守危険)、CWE/MISRAマッピング、--preset prでdiffのみ、AI補強
L2': 自己チェック(PR投げる前) c-review-ai(ブラウザ版、自社プロダクト) 環境構築不要、コピペで試せる軽量版
L3: チーム監査・MISRA準拠 CSAF(自社プロダクト) libclang AST + 依存グラフでrisk A/B/C自動昇格

make_channel()のようなpipe両端管理の設計ミスは、L2のcreviewで「closeなし」として拾えても、「そもそも呼び出し元にp[1]を返す設計に直すべきか」まではツールは判断してくれません。ここは【重大】ラベルを起点に人間が設計を見直す領域で、CSAFで依存グラフを見ながら「このpipeを使っている全呼び出し元がp[1]を握り潰しているか」を横断チェックするのがL3の役目です。

まとめ

  • 7件中3件はfd/socket/FILEのclose忘れ(CWE-401)、4件はNULL契約未表明(CWE-476)で、件数比は「リソース管理」と「契約表明」がほぼ同格の指摘対象であることを示しています。
  • read_first_byte()は正常系でcloseを書いているにもかかわらず異常系のreturnで漏れており、手動レビューは「closeが書かれている実績」を見て安心し、returnパスの本数を数え切れずに読み飛ばしやすいパターンです。
  • make_channel()のpipe両端管理は、指摘行(L37)と実際に直すべき行(p[1]を捨てている箇所)がずれるため、行番号だけを見て終わらせず前後の設計まで読む必要があります。
  • 4関数全ての第一引数・第一出力引数がNULL未検査であり、__attribute__((nonnull))を関数シグネチャに付けるだけで、呼び出し側の契約違反をコンパイル時に近い段階で洗い出せます。
  • 7件全てが【重大】ラベルという結果は、この検証コードがレビュー観点で軽視できない密度の問題を抱えていたことの裏付けであり、ラベルの偏り自体もレビュー優先度判断の材料になります。

試すリンク(無料で試す → 業務で監査する)

C言語の静的解析を、いきなり有料ではなく「無料で手を動かす → 日常運用 → 監査レベル」の順で用意しています。

  • まずブラウザで試す → c-review-ai(自社プロダクト / MIT / 無料)
    貼り付けた1ファイルをその場でレビュー。導入ゼロで挙動を確かめる用。
    → https://cutt.ly/pyuTypUW
  • CLI で日常運用 → creview(自社プロダクト / 無料)
    手元のコードをコミット前に単発チェック。CI にも組み込める。
    → https://cutt.ly/uyuTyst0
  • リポジトリ全体を監査 → CSAF(自社プロダクト / ¥4,980 買い切り)
    MISRA 準拠チェックと CWE データフロー解析をリポジトリ横断で走らせ、監査レポート(HTML)を出力。
    レビューの属人化を止めたいチーム向けで、商用の静的解析ツールなら数十万円かかる基本機能を、個人でも入れられる価格にしています。
    → https://cutt.ly/5tKoVA2h

無料の2つで「使える」と感じたら、リポジトリ全体に監査を回すのが CSAF です。バージョンは随時更新しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?