概要(TL;DR)
- Rust 1.91.1 における
dangling_pointers_from_localsの warn-by-default 化が、ローカル変数由来の生ポインタ返却を可視化。CIの条件コンパイル漏れにより、本番で SIGSEGV を誘発する深夜インシデントが発生。 - 本稿は発生経緯、lint の内部動作、典型的誤りパターン、修正例、複数の PlantUML 図解、および実務で即適用できる CI / チェックリストを含む完全防御手順を提供する。
目次
- はじめに(問題の背景)
- 影響要約(1.91.0 → 1.91.1)
- lint の内部動作(MIR トレース)
- 検知フロー図(PlantUML)
- 不正コードパターン(5例)と詳細な修正理由
- 条件コンパイルの落とし穴(深掘りと対策)
- 深夜インシデントの詳細時系列(PlantUML)
- CI/CD の完全防御設計(構成例+チェックリスト)
- チームルールと影響マッピング(表)
- パフォーマンス評価とトレードオフ
- まとめ(実務向けアクション項目)
1. はじめに(問題の背景)
2025年11月、FFI を多用するサービスで、unsafe ブロックと raw pointer が多用されたコードベースを保守している最中に起きたインシデントを記述します。cargo check により lint が5件検出され、ローカル環境では問題を検知できた一方、CI のターゲットマトリクスに Linux 条件が入っていなかったため本番デプロイで初めて致命的事象が発生しました。
読者想定:Rust で FFI / unsafe / low-level メモリ操作を扱う開発者、CI 設計者、運用担当
目的:同種の事故を再発させないための技術的解説と即時適用できる防御策の提示
2. Rust 1.91.0 → 1.91.1 の実務影響(改訂版)
| 変更カテゴリ | 主要変更点 | 実務影響 | 推奨対応(短期〜中期) |
|---|---|---|---|
| Lint |
dangling_pointers_from_locals が warn-by-default に昇格 |
★★★★★ |
deny へ昇格、CI で全ターゲット実行 |
| Lint |
unsafe_op_in_unsafe_fn 挙動調整 |
★★★ | unsafe 関数の内部レビュー強化 |
| ターゲット |
aarch64-pc-windows-msvc が Tier 1 |
★★★ | クロスコンパイルテスト追加(ARM) |
| Wasm |
#[wasm_import_module] の修正 |
★★★★ | Wasm を使うパスの再テスト |
| illumos |
File::lock 系修正 |
★★ | illumos を対象にする場合のみ対応 |
| stdlib | 軽微な UB 回避修正多数 | ★★ | 標準依存の挙動確認 |
注:上表は実務影響を主観的に評価したものです。組織のシステム構成によって優先度は変わります。
3. dangling_pointers_from_locals lint の内部動作(要点)
- 解析単位: コンパイルユニットの MIR を用いたローカル変数のライフタイム/アドレス到達性解析
-
検出対象: 関数スコープ終了後に無効化されるローカル値から生成され、関数外へエスケープするポインタ(生
*const/*mut) -
除外パターン: 明示的な所有権移譲(
Box::into_raw、Arc::into_rawなど)やstatic/OnceLockなどの静的ライフタイム確保
技術的補足:この lint は borrow checker(借用時検査)とは別に MIR レベルでの「アドレス到達性」をトラッキングするため、一部のパターン(mem::take などの中間操作)が誤検知に見える場合があります。とはいえ、誤検知を放置するより、明示的に所有権移譲を行う方が安全です。
4. 検知フロー図
5. 不正コードパターン(拡張版:7パターン)と修正版(理由付き)
下は実務で頻出するパターンを7件に拡張し、それぞれに対策と短い理論的説明を付記します。
| No | 悪い例(要点) | なぜダメか | 推奨修正(要点) |
|---|---|---|---|
| 1 |
v.as_mut_ptr() を返す(Vec) |
Vec のバッファはローカルスコープに属する |
Box::into_raw でヒープ所有へ移譲 |
| 2 | 固定長配列 .as_ptr() を返す |
スタックに置かれるため関数終了で無効 |
static または OnceLock を使って静的確保 |
| 3 | mem::forget(local); ptr |
forget は drop を抑止するだけでスコープ消滅は扱わない |
明示的に Box/Arc に入れて所有権移譲 |
| 4 | Box::new(local); b[0..].as_ptr() |
Box がローカルに束縛されたまま |
Box::into_raw で raw pointer を取得 |
| 5 | 再キャストや参照の再生(例:&*(ptr as *const T)) |
元のポインタがローカル由来だと検知される | 所有権移譲、または API 設計で生ポインタを返さない |
| 6 |
static mut に格納して返す |
static mut は unsafe で競合リスクが高い |
OnceLock / Mutex / Atomic を用いる |
| 7 | 条件コンパイルブロックでのみ存在する危険な実装 | CI がそのターゲットをカバーしていないと lint が無視される |
--all-targets / マトリクス CI を必須化 |
5.1 各修正版に対する実装例(抜粋)
1) Vec の case(修正例と説明)
// NG
unsafe fn bad_vec_ptr() -> *mut u8 {
let v = vec![0u8; 16384];
v.as_mut_ptr()
}
// OK
unsafe fn good_box_ptr(v: Vec<u8>) -> *mut u8 {
let boxed = v.into_boxed_slice();
Box::into_raw(boxed) as *mut u8
}
説明: 元の Vec のバッファは関数スコープの外に存在しないため、Box(ヒープ所有)に移して into_raw で所有権を明示する。
2) 配列の case(修正例)
use std::sync::OnceLock;
static GLOBAL_BUF: OnceLock<Vec<u8>> = OnceLock::new();
unsafe fn good_static_ptr() -> *const u8 {
GLOBAL_BUF.get_or_init(|| vec![0u8; 8192]).as_ptr()
}
説明: OnceLock を用い静的ライフタイムに持たせることで、返却したポインタは有効であり続ける。
3) mem::forget の誤解(修正例)
// NG
let mut local = Some(Vec::new());
let ptr = local.as_ref().unwrap().as_ptr();
std::mem::forget(local);
// -> lint は依然として警告する
// OK
let boxed = local.take().unwrap().into_boxed_slice();
let raw = Box::into_raw(boxed) as *const u8;
説明: mem::forget は drop を行わないが、変数スコープ自体のアドレス由来判定は残る。明示的移譲を行うのが確実。
6. 条件コンパイル(#[cfg])の盲点と対策
問題の要点
-
#[cfg]によりコンパイルユニットが切り替わるため、CI のターゲットに含まれないコードパスは lint やテストで検出されない。結果、あるターゲットでのみ危険なコードが生き残る。
実務対策(優先度付き)
-
必須: PR の CI に
--all-targetsとマトリクス(OS × ABI × アーキ)を追加する。 -
推奨:
cargo check --all-features --all-targets -- -D warningsを実行。 -
補助: 条件コンパイルで差分が大きいモジュールは
#[cfg(test)]によるユニットテストを各ターゲットで作成。 -
運用: 変更が
#[cfg]に影響する場合はレビューの際にターゲット影響表(簡易表)を必須添付。
7. 深夜インシデントの詳細時系列(改良 PlantUML)
8. CI/CD の完全防御設計(例と詳細チェックリスト)
以下は実務で即導入可能な GitHub Actions の一例と、それに紐づくチェックリストです。
8.1 CI ワークフロー(抜粋)
name: safety-net
on: [push, pull_request]
jobs:
matrix-safety:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-apple-darwin
- wasm32-unknown-unknown
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.91.1
with:
components: miri
- name: cargo check all targets
run: |
rustup target add ${{ matrix.target }} || true
cargo check --all-targets -- -D warnings
cargo check --target ${{ matrix.target }} -- -D warnings
- name: miri check (selective)
run: |
cargo miri test --all-targets --target ${{ matrix.target }} || true
- name: run unit tests
run: cargo test --target ${{ matrix.target }}
補足: Miri は全ターゲットで安定して動作しない場合があるため、失敗時も CI を止めずに通知を上げる運用にする場合は
|| trueを検討。ただし重要箇所は Miri を必須化すべき。
8.2 CI チェックリスト(PR 採用条件)
| チェック項目 | 必須/推奨 | 備考 |
|---|---|---|
cargo check --all-targets -D warnings |
必須 |
dangling_* を含む全警告を拒否 |
cargo test 各ターゲット |
必須 | プラットフォーム依存テストを実行 |
| Miri による検証 | 必須(unsafe を含む PR) | UB 検出のため必須 |
| Static analysis (clippy) | 推奨 |
clippy::undropped_raw_pointers 等の追加 |
| 依存クレートのセキュリティスキャン | 推奨 | OSS の脆弱性検出 |
9. チームルールと「問題→防御」マッピング(詳細テーブル)
| ルール | 内容 | 防御する課題 | 強制度 |
|---|---|---|---|
| R-01 |
dangling_pointers_from_locals を workspace.lints で deny
|
lint の見逃し、レビューのばらつき | ★★★★★ |
| R-02 | unsafe を伴う PR は --all-targets 実行必須 |
条件コンパイルでの見逃し | ★★★★★ |
| R-03 | Miri 必須 | 実行時 UB の早期検出 | ★★★★ |
| R-04 | FFI ポインタは所有権移譲を基本 | 生ポインタ長期保持のリスク | ★★★★ |
| R-05 | 条件コンパイル内の unsafe は責任者明記 | 設計意図の欠如 | ★★★ |
10. パフォーマンス評価(詳細)
| 構成 | 平均レイテンシ | メモリピーク | 備考 |
|---|---|---|---|
| 修正前(生ポインタ) | 2.84 ms | 1.8 GB | UB による SIGSEGV の発生あり |
| Box::into_raw 移行後 | 2.71 ms | 1.3 GB | レイテンシ 5% 改善、メモリ使用減少 |
| Arc::into_raw 共有化 | 2.78 ms | 1.4 GB | 参照カウントのオーバーヘッド微増 |
実測はプロダクション類似負荷での A/B テストに基づく値。実環境ではワークロードによる差分が出る。
11. Root Cause 分解表
| 要因分類 | 具体的事象 | なぜ見逃されたか | 恒久対策 |
|---|---|---|---|
| コード | linux_only_horror() が生ポインタを返却 | CI マトリクスに linux が無かった |
--all-targets を強制化、ターゲットマトリクス追加 |
| 運用 | 開発時に cargo check のみ実行 |
標準 target だけで ok と誤信 | PR テンプレートにチェック項目を追加 |
| ツール | lint は warn-by-default になったが deny ではない | ワーニング放置の慣習 | workspace.lints に deny 設定 |
12. 実務向けアクション項目
即時(24時間以内)
- workspace.lints に
dangling_pointers_from_locals = "deny"を追加 - PR の CI に
--all-targetsを追加 - 影響範囲のタグ付け(unsafe を含むモジュール一覧)
短期(1〜2 週間)
- Miri を用いた重点検証(重要な FFI 境界)
- PR テンプレート更新(チェックリストを明記)
- 条件コンパイルの影響範囲ドキュメント化
中期(1〜3 ヶ月)
- 主要アーキ/OS の CI マトリクス完成
- unsafe レビューガイドラインの整備と教育
- リスク高い箇所に対する Canary デプロイ戦術
付録 A: 参考実装スニペット(追加例)
- Box → raw pointer
fn into_raw_vec(v: Vec<u8>) -> *mut u8 {
let boxed = v.into_boxed_slice();
Box::into_raw(boxed) as *mut u8
}
- OnceLock を使った遅延初期化
use std::sync::OnceLock;
static BUF: OnceLock<Vec<u8>> = OnceLock::new();
fn global_ptr() -> *const u8 {
BUF.get_or_init(|| vec![0u8; 8192]).as_ptr()
}