RustをPixelベースバンドに導入する

投稿者:Jiacheng Lu、ソフトウェアエンジニア、Google Pixelチーム

Googleは、Pixelデバイスのセキュリティを継続的に向上させています。セルラーベースバンドモデムを悪用に対して強化することに注力しています。複雑なモデムファームウェア内のリスクを認識して、Pixel 9はメモリ安全性の脆弱性の様々に対する軽減策を搭載しました。Pixel 10では、Googleはプロアクティブセキュリティ対策をさらに進めています。「既存ファームウェアコードベースへのRustの展開」に関する以前の議論に続いて、本記事は具体的な適用例を共有しています。メモリ安全なRust DNS(ドメインネームシステム)パーサをモデムファームウェアに統合することです。新しいRustベースのDNSパーサは、リスクの高い領域の脆弱性全体のクラスを軽減することで、セキュリティリスクを大幅に削減し、他の領域でのメモリ安全コードのより広い採用の基礎を敷いています。

ここでは、この取り組みの経験を共有し、低レベル環境でメモリ安全言語の使用をさらに促進できることを期待しています。

モデムのメモリ安全性が待たない理由

近年、セルラーモデムに対する攻撃者およびセキュリティ研究者の関心が高まっています。例えば、GoogleのProject Zeroは、インターネット経由でPixelモデムでリモートコード実行を獲得しました。Pixelモデムには数十メガバイトの実行可能コードがあります。モデムの複雑性とリモート攻撃面を考慮すると、主にメモリ非安全なファームウェアコードに他の重大なメモリ安全性の脆弱性が残っている可能性があります。

なぜDNSなのか?

DNSプロトコルは、ブラウザがウェブサイトを検出するという文脈で最もよく知られています。セルラー技術の進化に伴い、最新のセルラー通信はデジタルデータネットワークに移行しています。その結果、通話転送などの基本的な操作もDNSサービスに依存しています。

DNSは複雑なプロトコルであり、信頼されていないデータの解析が必要であり、これはメモリ非安全言語での実装では脆弱性につながる可能性があります(例:CVE-2024-27227)。Rustでのパーサの実装は、メモリ非安全性に関連する攻撃面を減らすことによって価値を提供します。

DNSライブラリの選択

DNSは既にオープンソースRustコミュニティで一定レベルのサポートを受けています。DNSを実装している複数のオープンソースクレートを評価しました。以前の投稿で共有された基準に基づいて、hickory-protoが最適な候補であると判断しました。優れたメンテナンス、75%以上のテストカバレッジ、Rustコミュニティでの広範な採用があります。その普及率は、デファクトDNS選択肢としての可能性と長期的なサポートを示しています。hickory-protoは当初no_stdサポートを欠いていましたが、これはベアメタル環境に必要です(この話題に関する以前の投稿を参照)が、それと依存関係にサポートを追加することができました。

no_stdサポートの追加

hickory-protoのno_stdを有効にする作業はほぼ機械的です。我々はプロセスを以前の投稿で共有しました。hickory_protoとその依存関係の修正を実施して、no_stdサポートを有効にしました。アップストリームno_std作業は、他のプロジェクトに有益なno_std URLパーサにもなります。

上記のPRは、既存のstdのみクレートにno_stdサポートを拡張する方法の優れた例です。

コードサイズの研究

コードサイズは、使用するDNSライブラリを選択する際に評価した要因の1つです。

カテゴリ別
コードサイズ
DNSレスポンスを受信時にHickory-protoを呼び出すRust実装シム 4KB
core、alloc、compiler_builtins
(再利用可能、1回限りのコスト)
17KB
Hickory-protoライブラリと依存関係 350KB


プロトタイプを構築し、サイズ最適化設定を使用してサイズを測定しました。予想通り、hickory_protoは組み込み用途を念頭に設計されておらず、サイズ最適化されていません。Pixelモデムはメモリ制約がきつくないため、コミュニティサポートとコード品質を優先し、コードサイズの最適化は将来の作業として残しました。

ただし、追加のコードサイズは他の組み込みシステムのブロッカーになる可能性があります。これは将来的に、必要な機能のみを条件付きコンパイルするための追加機能フラグを追加することで対処できます。このモジュール性を実装することは、価値のある将来の作業になるでしょう。

モデムファームウェアへのRustの接続

RustのDNSライブラリを構築する前に、基本的な算術、動的割り当て、およびFFIをカバーするいくつかのRustユニットテストを定義して、Rustと既存のモデムファームウェアコードベースの統合を確認しました。

Rustコードをstaticlibにコンパイル

cargoを使用することはRustエコシステムでのコンパイルのデフォルトの選択肢ですが、既存のビルドシステムへの統合時に課題を提示しています。2つのオプションを評価しました:

  1. cargoを使用してstaticlibをビルドしてからモデムをビルドします。次に、生成されたstaticlibをリンクステップに追加します。
  2. rustcと直接連携して、Rustコンパイルステップを既存のモデムビルドシステムに統合します。

将来的により多くのRustコンポーネントを追加する場合、オプション#1はスケールしません。複数のstaticlibのリンクは重複シンボルエラーを引き起こす可能性があります。オプション#2を選択しました。これはより簡単にスケールでき、既存のビルドシステムへのより緊密な統合を可能にします。既存のC/C++コードベースは、Pigweedを使用して主要なビルドシステムを駆動しています。Pigweedは、Rustターゲットをサポートし、rustcへの直接呼び出しをGNで定義されたrustツールを通じて実行します。

hickory-protoとその依存関係、およびcore、compiler_builtin、allocを含むすべてのRustクレートをrlibにコンパイルしました。次に、すべてのrlibクレートをextern crateキーワードを使用して参照する単一のlib.rsファイルでstaticlibターゲットを作成しました。

core、alloc、およびcompiler_builtinsをビルド

Androidのrustツールチェーンは、corealloc、およびcompiler_builtinsのソースコードを配布し、モデムに対してこれを活用しました。これらは、crate_rootをそれぞれのクレートのルートlib.rsを指すようにGNターゲットを追加することで、ビルドグラフに含めることができます。

Pixelモデムファームウェアは、動的メモリ割り当てをサポートするための、よくテストされた特殊なグローバルメモリ割り当てシステムを既に持っています。allocサポートは、割り当てのCAPIへのFFI呼び出しを使用してGlobalAllocを実装することで追加されました:

use core::alloc::{GlobalAlloc, Layout};
extern "C" {
    fn mem_malloc(size: usize, alignment: usize) -> *mut u8;
    fn mem_free(ptr: *mut u8, alignment: usize);
}
struct MemAllocator;
unsafe impl GlobalAlloc for MemAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        mem_malloc(layout.size(), layout.align())
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        mem_free(ptr, layout.align());
    }
}
#[global_allocator]
static ALLOCATOR: MemAllocator = MemAllocator;

Pixelモデムファームウェアは、グローバルクラッシュハンドラとしてPigweed クラッシュフォーサードのバックエンドを既に実装しています。FFIを通じてRust panic_handlerに公開することで、RustとC/C++コード両方のクラッシュハンドリングを統一します。

#![no_std]
use core::panic::PanicInfo;
extern "C" {
    pub fn PwCrashBackend(sigature: *const i8, file_name: *const i8, line: u32);
}
#[panic_handler]
fn panic(panic_info: &PanicInfo) -> ! {
    let mut filename = "";
    let mut line_number: u32 = 0;
    if let Some(location) = panic_info.location() {
        filename = location.file();
        line_number = location.line();
    }
    let mut cstr_buffer = [0u8; 128];
    // Never writes to the last byte to make sure `cstr_buffer` is always zero
    // terminated.
    let (_, writer) = cstr_buffer.split_last_mut().unwrap();
    for (place, ch) in writer.iter_mut().zip(filename.bytes()) {
        *place = ch;
    }
    unsafe {
        PwCrashBackend(
            "Rust panic\0".as_ptr() as *const i8,
            cstr_buffer.as_ptr() as *const i8,
            line_number,
        );
    }
    loop {}
}

Rustスタティックライブラリをリンク

Pixelモデムファームウェアのリンクには、リンカを呼び出してC/C++コードから生成されたすべてのオブジェクトをリンクするステップがあります。llvm-ar -xを使用してRustの結合スタティックライブラリからオブジェクトファイルを抽出し、それらをリンカに供給することで、Rustコードは最終的なモデムイメージに表示されます。

リンク中の弱いシンボルが原因で、パフォーマンスの問題が発生しました。Rust coreおよびcompiler-builtinの含有は、様々なテストで予期しない電力およびパフォーマンスの回帰を引き起こしました。分析の結果、モデムファームウェアから提供されるモデム最適化実装memsetおよびmemcpyが誤ってcompiler_builtinで定義されたものに置き換わっていることが判明しました。これは、compiler_builtinクレートと既存のコードベースの両方が弱いシンボルとして定義されているため、リンカはどちらがより弱いのかを判断する方法がないため、発生しているようです。1行のシェルスクリプトを使用してリンク前にcompiler_builtinクレートを削除することで回帰を修正しました。

llvm-ar -t <rust staticlib> | grep compiler_builtins | xargs llvm-ar -d <rust staticlib>

hickory-protoの統合

RustAPIを公開してC++にコールバック

DNSパーサの場合、DnsレスポンスパーサAPIをCで宣言し、同じAPIをRustで実装しました。

int32_t process_dns_response(uint8_t*, int32_t);

Rust関数は、エラーコードを表す整数を返します。DNSレスポンス内で受信したDNS回答は、元のC実装と結合されたメモリ内データ構造に更新される必要があるため、既存のC関数を使用してこれを実行します。既存のC関数はRust実装からディスパッチされます。

pub unsafe extern "C" fn process_dns_response(
    dns_response: *const u8,
    response_len: i32,
) -> i32 {
    //... validate inputs `dns_response` and `response_len`.
    // SAFETY:
    // It is safe because `dns_response` is null checked above. `response_len`
    // is passed in, safe as long as it is set correctly by vendor code.
    match process_response(unsafe {
        slice::from_raw_parts(dns_response, response_len)
    }) {
         Ok(()) => 0,
         Err(err) => err.into(),
    }
}
fn process_response(response: &[u8]) -> Result<()> {
    let response = hickory_proto::op::Message::from_bytes(response)?;
    let response = hickory_proto::xfer::DnsResponse::from_message(response)?;
    for answer in response.answers() {  
        match answer.record_type() {
            hickory_proto::RecordType:... => {
                // SAFETY:
                // It is safe because the callback function does not store
                // reference of the inputs or their members.
                unsafe {
                    callback_to_c_function(...)?;
                }
            }
            // ... more match arms omitted.
        }    
    }
    Ok(())
}

このケースでは、DNSレスポンスパーサAPIは簡単で手書きできるほど単純ですが、レスポンス処理のためのC関数へのコールバックは複雑なデータ型変換があります。したがって、bindgenを活用してコールバック用のFFIコードを生成しました。

サードパーティクレートをビルド

すべての機能が無効化されても、hickory-protoは30以上の依存クレートを導入します。手動で記述されたビルドルールは、正確性を保証することは困難で、依存関係を新しいバージョンにアップグレードするときに拡張性は低いです。

Fuchsiaはcargo-gnawを開発して、サードパーティRustクレートのビルドをサポートしています。Cargo-gnawcargo metadataを起動して依存関係を解決し、GNビルドルールを解析して生成することで動作します。これにより、正確性と保守性が容易になります。

結論

Pixel 10シリーズのスマートフォンは、メモリ安全言語をそのモデムに統合する最初のPixelデバイスになることで、重要な瞬間を示しています。

1つのリスクの高い攻撃面の置き換えは、それ自体が価値がありますが、このプロジェクトは、将来のメモリ安全パーサーとコードのセルラーベースバンドへの統合の基礎を敷いており、ベースバンドのセキュリティ態勢が開発が続く限り改善し続けることを保証します。

Armando Montanez、Bjorn Mellem、Boky Chen、Cheng-Yu Tsai、Dominik Maier、Erik Gilling、Ever Rosales、Hungyen Weng、Ivan Lozano、James Farrell、Jeffrey Vander Stoep、Jiacheng Lu、Jingjing Bu、Min Xu、Murphy Stein、Ray Weng、Shawn Yang、Sherk Chung、Stephan Chen、Stephen Hinesに特別に感謝します。

翻訳元: http://security.googleblog.com/2026/04/bringing-rust-to-pixel-baseband.html

ソース: security.googleblog.com