プログラミング - TECH PLAY - TECH PLAY

TECH PLAY

プログラミング

イベント

マガジン

技術ブログ

なぜC++/JS間のメモリ管理は難しいのか 皆様、はじめまして。グローバルプロダクト開発本部 グローバルシステム部 技術推進ブロックの村木と申します。私たちのチームでは、OpenCVによる画像処理やニューラルネットワークによる画像認識を含む大規模なC++資産を、EmscriptenでWebAssemblyにコンパイルし、ブラウザへ移植するプロジェクトに取り組んでいます。 その中で私にとって難所となったのが、メモリ管理の理解でした。Emscriptenを使用する開発では、メモリの考え方が異なる2つの言語、C++とJavaScriptをまたいだ処理を実装することがあります。C++ではオブジェクトの寿命がコンストラクタとデストラクタによるRAIIで制御されるのに対し、JavaScriptではオブジェクトの寿命をGC(ガベージコレクタ)が管理し、通常、開発者が解放を意識することはありません。 このメモリモデルの違いに加え、C++のオブジェクトをJavaScript側へ渡すとき、その寿命や所有権に関する情報は引き継がれないことを、しっかりと理解しておく必要があります。 JavaScript側から見ると、C++のオブジェクトは単なる数値(メモリ上のアドレス)に過ぎず、この「数値」には、「誰が確保したのか」「いつまで有効なのか」「誰が解放すべきか」といった所有権の情報は含まれていません。また、JSのGCはC++側のメモリ領域を認識できません。 そのため、Wasmの境界を越えるオブジェクトについては、所有者と有効期間、解放の責任を明確にしたうえで実装する必要があります。この点を理解しておくことが、Emscriptenにおける開発の重要なポイントでした。 Emscriptenが提供するembindを使えば、C++のクラスや std::vector をJavaScriptのオブジェクトに近い形で扱えます。ただし、その抽象化されたAPIの背後で、オブジェクトの受け渡しと寿命管理がどのように行われているのかを理解しておく必要があります。 本記事では、まずWasmのメモリモデルの基礎を概観し、その上でembindによる実装を詳しく解説します。 目次 なぜC++/JS間のメモリ管理は難しいのか 目次 Wasmのメモリモデル C++オブジェクトの実体 境界を越えると寿命の情報が失われる embindは何を隠し、何を隠さないのか value_objectはコピー class_はハンドル typed_memory_viewはビュー おわりに Wasmのメモリモデル WebAssembly(Wasm)のメモリモデルは非常にシンプルです。Wasmモジュールは、 Linear Memory(線形メモリ) と呼ばれる、単一の連続したバイト列を自身のメモリ空間として持ちます。この線形メモリは、JavaScript側からは WebAssembly.Memory オブジェクトの buffer として見えます(型は ArrayBuffer )。 C++コードがWasmにコンパイルされると、グローバル変数やスタック領域、そして malloc / new によって確保されるヒープ領域は、すべてこの線形メモリの中に配置されます。したがって、C++オブジェクトの「実体」とは、この線形メモリ上の特定のアドレスから始まる領域にほかなりません。JavaScriptの視点で言い換えれば、C++のオブジェクトは、巨大な ArrayBuffer (= WebAssembly.Memory が内部に保持する一本の配列)の中に、バイト列として格納されているのです。 まずはこのことを、簡単なコードで確認していきます。 C++側で「指定されたアドレスから始まるバイト列の合計値を計算する」関数を実装します。 // C++側:ptrは線形メモリ内のオフセット(アドレス) extern "C" int sum_bytes ( const uint8_t * ptr, int len) { int total = 0 ; for ( int i = 0 ; i < len; i++) total += ptr[i]; return total; } JavaScript側では、Emscriptenが提供する _malloc で線形メモリ上の領域を確保し、 HEAPU8 を通じて値を書き込みます。このとき、関数に渡す ptr は単なる数値(アドレス)です。 const bytes = new Uint8Array ([ 10 , 20 , 30 ]) ; // 1. 線形メモリ上に3バイト確保 const ptr = Module . _malloc ( bytes . length ) ; // 2. JSから線形メモリへ書き込む Module . HEAPU8 . set ( bytes , ptr ) ; // 3. C++側で同じメモリ領域を読み取って合計を計算 const total = Module . _sum_bytes ( ptr , bytes . length ) ; // 4. メモリを解放 Module . _free ( ptr ) ; console . log ( total ) ; // 60 ptr はC++から見れば「ポインタ」ですが、JSから見れば HEAPU8 の「添字(number)」に過ぎません。 HEAPU8[ptr] と記述することで、C++側がポインタを通じてアクセスするのと、全く同じメモリ領域をJS側でも参照できます。 C++オブジェクトの実体 EmscriptenでコンパイルされたC++コードにおいて、 new や std::vector などによる動的なメモリ確保は、すべてWasmの線形メモリ内(のヒープ領域)に対して行われます。JS側から呼び出す _malloc も、同じヒープ領域からメモリを切り出します。 C++オブジェクトの「実体」とは、linear memory上の特定のアドレスから始まる、数バイトから数メガバイトの領域のことです。例えば、以下のクラスを考えます。 class ByteBuffer { public : explicit ByteBuffer ( size_t n) : data_ (n, 0 ) {} size_t size () const { return data_. size (); } private : std :: vector < uint8_t > data_; // 中身はヒープ上の別領域を指す }; new ByteBuffer(1024) を実行すると、まず ByteBuffer 本体(vectorのポインタやサイズ情報など)がヒープに確保され、その内部のvectorがさらに1024バイトをヒープに確保します。これらはいずれもlinear memory内に存在します。 C++の中で完結している限り、この寿命管理は言語仕様によって保証されています。スコープを抜ければスタック上のオブジェクトは破棄され、 std::vector が確保したメモリも自動的に解放されます。 delete を呼び出せばデストラクタが連鎖し、メモリは安全に回収されます。つまり、 RAII(Resource Acquisition Is Initialization)とデストラクタの仕組みが、メモリの確保と解放の対応を担保している のです。 しかし、この保証が有効なのは「C++のライフタイム管理が届く範囲にある」間だけです。オブジェクトへの参照がJS側に渡ると、この前提は崩れます。 境界を越えると寿命の情報が失われる JavaScriptでは、どこからも参照されなくなったオブジェクトはGC(ガベージコレクション)によって自動的に回収されます。しかし、GCが寿命を管理できるのは「JSヒープ上にあるJSの値」だけです。C++オブジェクトの実体は、前述のとおり線形メモリ(巨大な ArrayBuffer )の一領域にすぎず、GCから見れば「生存している1つの大きな箱」の内側でしかありません。その箱の内部のどこが有効で、どこが解放済みなのかを、GCは関知しないのです。 そのため、C++オブジェクトのアドレスを保持していたJS変数が使われなくなり、GCに回収されたとしても、線形メモリ上の実体は確保されたまま残り続けます。C++のデストラクタは呼ばれず、メモリはリークします。 逆のケースも同様で、C++側で free を呼んでメモリを解放しても、JS変数が握っているアドレス(ただの数値)はそのまま残ります。すでに無効になった領域を指し続けるその数値を使えば、解放済みメモリを読み書きする「Use-after-free」を引き起こします。 いずれもC++/JS間の境界を越えた際に、「このアドレスはByteBufferを指し、所有権はこちらにある」という型と所有権のメタ情報が単なる数値に変換されて失われます。その結果、「誰がいつ解放するのか」という所有権の所在が型システムからもランタイムからも見えなくなり、問題が発生します。 embindは何を隠し、何を隠さないのか これまで、WebAssemblyとJavaScript間でデータをやり取りするために、以下の低レイヤーな手順が必要であることを確認しました。 _malloc でメモリを確保する HEAPU8.set 等を介して手動でデータをコピーする 確保したメモリのアドレスを数値として管理する embindは、こうした低レイヤーの「配管」処理を宣言的な記述に置き換え、JavaScriptとC++の間のバインディング実装を省力化可能な、Emscriptenの強力な機能です。 embindを有効にするには、ビルド時に -lembind オプションを付与します。利用にあたっては、C++側で EMSCRIPTEN_BINDINGS ブロックを記述し、公開したい関数やクラスを明示します。 #include <emscripten/bind.h> using namespace emscripten; // 構造体:単純な値のセット struct Point { float x; float y; }; Point midpoint ( const Point& a, const Point& b) { return Point{(a.x + b.x) / 2 , (a.y + b.y) / 2 }; } // クラス:状態を持つオブジェクト class ByteBuffer { public : explicit ByteBuffer ( size_t n) : data_ (n, 0 ) {} size_t size () const { return data_. size (); } void fill ( uint8_t v) { std :: fill (data_. begin (), data_. end (), v); } private : std :: vector < uint8_t > data_; }; EMSCRIPTEN_BINDINGS (demo) { // 構造体をJSのプレーンなオブジェクトとしてマッピング value_object<Point>( "Point" ) . field ( "x" , &Point::x) . field ( "y" , &Point::y); function ( "midpoint" , &midpoint); // クラスをJSのクラスとしてマッピング class_<ByteBuffer>( "ByteBuffer" ) .constructor< size_t >() . function ( "size" , &ByteBuffer::size) . function ( "fill" , &ByteBuffer::fill); // 型登録(JSから操作可能にする) register_vector< float >( "VectorFloat" ); } この定義により、JavaScript側からは、あたかもネイティブなオブジェクトを扱うかのように、C++側の機能を利用できます。 // JS側からの利用例 const m = Module . midpoint ({ x : 0 , y : 0 } , { x : 10 , y : 4 }) ; console . log ( m . x , m . y ) ; // 5 2 const buf = new Module . ByteBuffer ( 1024 ) ; buf . fill ( 0xff ) ; console . log ( buf . size ()) ; // 1024 embindを利用することで、 _malloc による手動メモリ確保や HEAPU8 の直接操作といった低レベルな記述は隠蔽され、コードも簡潔になります。 しかし私は、前節で述べた低レイヤーにおける処理の理解は重要だと考えています。私自身について言えば、前節で見た線形メモリの挙動を一度自分の手で確かめることで、ようやくembindの宣言的な記法を使いこなせるようになったからです。また、いつハンドルを delete() すべきか、なぜ確保したはずのビューが壊れることがあるのかといった疑問にも、筋道を立てて考えられるようになりました。 以降では、embindを使ってJS/C++間でデータを受け渡す手法を見ていきます。この観点で整理すると、本記事が扱う代表的なパターンは、 コピー 、 ハンドル 、 ゼロコピーのビュー の3つに分けられます。 value_object はコピー value_object<Point> として公開した構造体は、JS/C++の境界を越えるたびに、JSのプレーンなオブジェクトへ詰め替えられます。逆方向も同じです。そのため、返ってきた m はC++側の実体への参照ではなく、独立した複製です。たとえば m.x = 99 と書き換えてもC++側には影響しません。使い終わったあとは、通常のJSオブジェクトと同じようにGCによって回収されます。 つまり、 value_object は、所有権を意識せずに扱える一方で、JS/C++の境界を越えるたびにコピーが発生する仕組みです。 この仕組みは、座標や計測値のような小さな値のまとまりを扱うのに向いています。一方で、大きな配列を内包する構造体を毎フレーム往復させるような用途には向きません。 また、 value_object は純粋な値の束であることが前提です。フィールドに生ポインタや所有権を持つリソースを含める設計は避けるべきです。コピーされた先で、そのポインタが何を指しているのか、誰が解放するのかという情報が失われ、単なるアドレス値だけが残ってしまうからです。 class_ はハンドル new Module.ByteBuffer(1024) を実行すると、C++のヒープ、すなわちlinear memory内に ByteBuffer の実体が new されます。JS側に返ってくるのは、その実体を指すハンドルを内蔵した薄いラッパーオブジェクトです。 第1部で見たとおり、C++オブジェクトの実体はlinear memory上にあります。JSの変数が保持しているのは、その実体そのものではなく、実体を指すハンドルです。 buf.fill(0xff) を呼ぶたびに、呼び出しはJSからWasm上の同じC++実体に届きます。ここではコピーは起きません。 その代わり、 実体の解放はJS側が引き受け、明示的に delete() を実行する 必要があります。 register_vector で公開した std::vector も、基本的には class_ と同じ系統です。返ってくるのは素のJS配列ではなく、 .size() 、 .get(i) 、 .set(i, x) 、 .push_back(x) などを持つハンドルです。 この仕組みのため、要素アクセスのたびにJSからC++側への呼び出しが発生します。よって、巨大な配列を .get(i) のループで一要素ずつ走査すると、JS/C++間呼び出しのコストがその回数分だけ積み上がります。また、ハンドル自体には明示的な破棄が必要です。 要素そのものはコピーによって安全にやり取りされますが、ハンドルの後始末はJS側が担う必要があります。次に見る typed_memory_view と対比すると、 register_vector は「中身の扱いは安全寄りだが、破棄の責務は残る配列」と位置づけられます。 typed_memory_view はビュー typed_memory_view が提供するのは、ゼロコピーのアクセスです。C++側の連続領域を一切複製せず、JSの TypedArray としてそのまま覗かせます。たとえば、カメラ画像のようなRGBAバッファを返す例を考えてみます。 #include <emscripten/val.h> class FrameBuffer { public : FrameBuffer ( int w, int h) : pixels_ (w * h * 4 , 0 ) {} // 内部バッファを指すビューを返す。コピーは発生しない emscripten::val pixels () { return emscripten:: val ( emscripten:: typed_memory_view (pixels_. size (), pixels_. data ())); } private : std :: vector < uint8_t > pixels_; // RGBA }; const frame = new Module . FrameBuffer ( 640 , 480 ) ; const view: Uint8Array = frame . pixels () ; // linear memoryを直接指す窓 view [ 0 ] = 255 ; // C++側のpixels_[0]が直接書き換わる この view の正体は、第1部で見た HEAPU8 の部分ビューです。linear memory上の pixels_.data() から始まる領域を、JSの Uint8Array として直接見ているだけです。コピーが発生しないため、メガバイト級のバッファでもほぼ一瞬でJS側へ「渡す」ことができます。 ただし、ビューはあくまで「ビュー」なため、その有効期限は、元のバッファの寿命に完全に支配されます。具体的には、次のいずれかが起きた時点でビューは無効になります。 元のオブジェクト、つまり frame が破棄された → viewは解放済み領域を指すことになる 内部の std::vector が再確保された → viewは古いアドレスを指したままになる 線形メモリが成長した 第1部で予告したとおり、 ArrayBuffer が差し替わり、古いビューはdetachされる。成長は別の場所の malloc をきっかけに起こることもあるため、自分のコードがそのバッファに何もしていなくても、ビューが無効になる可能性がある。 したがって typed_memory_view の鉄則は「 取得したら即座に読み書きし、持ち越さない 」です。保持して使用し続けたいデータは、 view.slice() などでその場でJS側へコピーし、切り離しておく必要があります。 3つの渡し方をまとめると、次のように整理できます。 コピー( value_object )は、所有権を考えなくてよい代わりにコピーコストを払う。 ハンドル( class_ / register_vector )は、コピーを避けられる代わりに、JS側が解放の責務を負う。 ビュー( typed_memory_view )は高速だが、元バッファの寿命と完全に結びつく。 おわりに 本記事では、まずWasmの線形メモリという土台を確認し、その上でembindが提供する3つのデータの渡し方を見てきました。 embindの宣言的なAPIは、 _malloc や HEAPU8 といった低レイヤーを隠蔽してくれます。しかし所有権と寿命の責務は、プログラマが十分に認識し、管理する必要があります。本記事では、低レイヤーの挙動から順を追って解説することで、embindを実装に採用した場合も、そのAPIの背後で所有権と寿命がどう扱われているかを見通せるようになることを目指しました。 同じようにEmscriptenと向き合う方にとって、本記事がメモリ管理を筋道立てて考えるための一助となれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
本ブログは 2026 年 7 月 30 日に公開された AWS Blog “ Extend Amazon Inspector SBOM Generator with Plugins ” を翻訳したものです。 Amazon Inspector は、 Amazon Web Services (AWS) のワークロードを継続的にスキャンしてソフトウェアの脆弱性を検出する、自動化された脆弱性管理サービスです。Amazon Inspector の脆弱性管理機能は、 Amazon Inspector SBOM Generator (inspector-sbomgen) と呼ばれる資産インベントリエンジンによって支えられています。これはスタンドアロンのコマンドラインツールで、コンテナイメージ、ディレクトリ、アーカイブ、ローカルシステム、コンパイル済みバイナリなどから ソフトウェア部品表 (SBOM) を生成します。過去 2 年間で、AWS は inspector-sbomgen のカバレッジを数十のプログラミング言語エコシステム、オペレーティングシステム、広く導入されているアプリケーションへと拡大してきました。 今回、inspector-sbomgen を利用するビルダー向けの新機能として、独自のカスタムパッケージコレクターを記述できる プラグインシステム を発表します。ソースコードのコンパイルや公式リリースを待つ必要はなく、すぐに使い始めることができます。 inspector-sbomgen の最新バージョンは、 Amazon Inspector ユーザーガイド からダウンロードできます。 この記事では、inspector-sbomgen プラグインシステムでできること、これを構築した理由、そして数分で最初のプラグインを書く方法を紹介します。あわせて、プラグインが生成したパッケージコンポーネントを Amazon Inspector の脆弱性スキャンと統合する方法や、セキュリティが強化された予測可能なプラグイン動作を実現するプラグインの安全性モデルについても解説します。 プラグインシステムを構築した理由 ソフトウェアのエコシステムは動的です。新しい言語パッケージマネージャー、ロックファイル形式、エンドユーザーアプリケーションが絶えずリリースされ、その多くは迅速に採用されます。中にはセキュリティの検証がほとんど行われないまま使われるものもあります。その結果、セキュリティチームには可視性のギャップが残ります。つまり、SBOM ツールがまだ認識できないソフトウェアが本番ワークロードで動いているという状態です。お客様からは、こうしたエコシステムの多くを直接インベントリ化したいという要望をいただいてきました。最近まで、それを実現する唯一の方法は、機能リクエストを出して inspector-sbomgen チームがエコシステムに対応し、新しいリリースをデプロイするのを待つことでした。 inspector-sbomgen プラグインシステムは、この状況を変えます。プラグインを使うと、次のことができます。 inspector-sbomgen が標準では対応していないエコシステムへの対応 – 新しいオープンソースエコシステム、ニッチまたは変化の速いパッケージ形式、組織独自のツールなど、inspector-sbomgen を変更することなくインベントリ化できます エコシステム検出の迅速なプロトタイピング – 開発者にも AI コーディングアシスタントにも扱いやすいプラグインシステムを設計しました。プラグインは Lua で記述され、実行時にロードされるため、Go ツールチェーンもコンパイルも不要です。組み込みのテストハーネスを使ってプラグインを繰り返し改善し、すぐに結果を確認できます 安定した基盤の上での構築 – プラグイン API はアーティファクトの種類による違いを抽象化するため、検出ロジックを一度書くだけで、コンテナイメージ、アーカイブ、ローカルシステムなどでシームレスに動作します。また、プラグインは sbomgen の内部構造から分離されているため、コアツールでリグレッションが発生した場合の影響範囲も小さく抑えられます 実際、私たち自身もこのプラグインシステムを内部で活用し、新しいエコシステムのカバレッジを以前より速く提供できるようになりました。 1.13 リリース では、Apache Tomcat、NGINX、MySQL、Redis、WordPress、OpenSSH ツールチェーンなど、これまで Go で実装されていた 20 以上のエコシステムが、プラグインとして sbomgen バイナリに組み込まれています。同じリリースでは、Apache Cassandra、Apache Struts、Conda、Swift パッケージ、AI エージェントコレクター (Amazon Q Developer、Kiro CLI、Claude Code、GitHub Copilot、Ollama) など、10 を超える新しいエコシステムもプラグインとして追加されました。 inspector-sbomgen プラグインの仕組み sbomgen プラグインは 2 段階のパイプラインで動作します。 検出 (discovery) – アーティファクトのファイルシステムをスキャンし、インストール済みパッケージのメタデータを含むファイルを特定します 収集 (collection) – 検出された各ファイルを開き、ファイルの内容を解析して、結果を SBOM にパブリッシュします 内部では、イベントバスが検出プラグインと収集プラグインをつないでいます。検出プラグインは検出したファイルの一覧をイベントとしてパブリッシュし、1 つ以上の収集プラグインがそのイベントをサブスクライブして、パッケージ収集をトリガーします。開発者にとっては、これは オブザーバーパターン としておなじみの動作でしょう。 この分離により、1 つの検出プラグインが複数のコレクターにデータを供給できます。例えば、あるコレクターはパッケージメタデータを抽出し、別のコレクターはシークレットをスキャンし、さらに別のコレクターはポリシーをチェックする、といった構成が可能です。各収集プラグインは、計算コストの高いアーティファクトファイルシステムの再走査を行うことなく、同じファイルリストを利用できます。 5 分で書ける最初のプラグイン inspector-sbomgen を使えば、プラグイン環境を簡単にセットアップできます。 plugin new コマンドで sbomgen に新しいプラグインワークスペースを作成させ、 --with-example フラグを指定すると、すぐに実行できる検出プラグインと収集プラグインのペアがワークスペースに用意されます。 inspector-sbomgen plugin new --with-example 上記のコマンドを実行すると、プラグイン名と、プラグインワークスペースを格納するディレクトリの入力を求められます。カスタム値を指定することも、デフォルト値をそのまま使うこともできます。 Plugin name (identifies the software ecosystem your plugin will inventory, e.g. debian-dpkg, rhel-rpm, python-pip, cmake) [my-custom-ecosystem]: <enter> Project directory [my-sbomgen-plugins]: <enter> Created plugin "my-custom-ecosystem" in my-sbomgen-plugins/ なお、対応するコマンドラインインターフェイス (CLI) 引数でプラグイン名とディレクトリを指定すれば、対話形式のプロンプトをスキップできます。 inspector-sbomgen plugin new \ --with-example \ --name my-custom-ecosystem \ --path my-sbomgen-plugins プラグインワークスペースを作成すると、inspector-sbomgen は次のステップを案内する画面を表示します。開発者や AI コーディングアシスタントに対して、変更が必要なソースファイルや関連ドキュメントの場所を示してくれます。 Next steps: Get started: 1. Open plugin folder in a code editor (VS Code recommended) 2. Add test files that your plugin will discover and parse (e.g., config files, lockfiles, binaries, etc.): my-sbomgen-plugins/discovery/cross-platform/extra-ecosystems/my-custom-ecosystem/_testdata/ Develop: 3. Edit discovery: my-sbomgen-plugins/discovery/cross-platform/extra-ecosystems/my-custom-ecosystem/init.lua 4. Edit collection: my-sbomgen-plugins/collection/cross-platform/extra-ecosystems/my-custom-ecosystem/init.lua Test: 5. Write unit tests: my-sbomgen-plugins/discovery/cross-platform/extra-ecosystems/my-custom-ecosystem/init_test.lua 6. Run unit tests: inspector-sbomgen plugin test --path my-sbomgen-plugins Deploy: 7. Distribute your plugin directory wherever you run inspector-sbomgen: inspector-sbomgen <arguments> --plugin-dir /path/to/my-sbomgen-plugins Example: inspector-sbomgen container --image alpine:latest -o /tmp/sbom.json --plugin-dir /path/to/my-sbomgen-plugins For code completion, install the VS Code Lua language server extension: https://luals.github.io/#vscode-install For more information: - Plugin guide: my-sbomgen-plugins/docs/sbomgen-plugin-developer-guide.md - Testing guide: my-sbomgen-plugins/docs/sbomgen-plugin-testing-guide.md - API reference: my-sbomgen-plugins/docs/sbomgen-plugin-api-reference.md - Documentation: https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html プラグインワークスペースができたので、その中身を詳しく見てみましょう。 tree my-sbomgen-plugins ├── AGENTS.md ├── collection │   └── cross-platform │   └── extra-ecosystems │   └── my-custom-ecosystem │   └── init.lua ├── discovery │   └── cross-platform │   └── extra-ecosystems │   └── my-custom-ecosystem │   ├── _testdata │   │   ├── empty │   │   └── example.lock │   ├── init_test.lua │   └── init.lua ├── docs │   ├── sbomgen-plugin-api-reference.md │   ├── sbomgen-plugin-developer-guide.md │   └── sbomgen-plugin-testing-guide.md ├── library │   └── sbomgen.lua └── README.md スキャフォールディングされたプロジェクトには、動作する検出プラグインと収集プラグインのペア、 _testdata/ 配下のテストフィクスチャを使ってパスするユニットテスト、統合開発環境 (IDE) 連携用の .vscode/settings.json 、開発者ドキュメントのローカルコピーが含まれています。 スキャフォールディングは、人間と AI コーディングアシスタントの両方が読みやすいように、意図的に簡潔で完結した内容になっています。各ファイルには、それぞれの関数の役割と、プラグイン作成者が記述すべき箇所を説明する明確なコメントが付いています。 プラグインをテストするには、まずパッケージロックファイルやコンパイル済みバイナリなど、スキャン対象となるものが必要です。サンプルプラグインは、次の内容を持つ架空の example.lock をインベントリ化します。 my-package-alpha==1.0.0 my-package-beta==2.3.1 my-package-gamma==0.9.5 付属の検出プラグインは、アーティファクトのファイルシステム内で example.lock のインスタンスを探す方法を知っています。 -- my-custom-ecosystem discovery plugin -- Discovers example.lock files in the artifact file list. function discover() return sbomgen.find_files_by_name({"example.lock"}) end そして、付属の収集プラグインは、 example.lock の内容を解析し、パッケージ情報を出力 SBOM にパブリッシュする方法を知っています。 -- my-custom-ecosystem collection plugin -- Parses example.lock files and extracts package name and version. function collect(file_path) local content = sbomgen.read_file(file_path) if content == nil then return end for line in content:gmatch("[^\n]+") do local name, ver = line:match("^(.+)==(.+)$") if name and ver then sbomgen.push_package({ name = name, version = ver, purl_type = "generic", namespace = "my-custom-ecosystem", component_type = sbomgen.component_types.APPLICATION, }) end end end テストの実行 プラグインにはテストフレームワークが組み込まれているため、実際のアーティファクトをスキャンする前にロジックを検証できます。テストは Lua で記述し、プラグインと同じ場所の init_test.lua に配置して、 _testdata/ 内のフィクスチャデータを参照します。 function test_discovers_packages() local result = testing.scan_directory("_testdata") testing.assert_equals(3, #result.findings) testing.assert_equals("my-package-alpha", result.findings[1].name) testing.assert_equals("1.0.0", result.findings[1].version) end function test_no_findings_for_empty_directory() local result = testing.scan_directory("_testdata/empty") testing.assert_equals(0, #result.findings) end 次のコマンドでテストを実行します。 inspector-sbomgen plugin test --path my-sbomgen-plugins -v === RUN my-custom-ecosystem/discovery/init_test/test_discovers_packages --- PASS: my-custom-ecosystem/discovery/init_test/test_discovers_packages (0.04s) === RUN my-custom-ecosystem/discovery/init_test/test_no_findings_for_empty_directory --- PASS: my-custom-ecosystem/discovery/init_test/test_no_findings_for_empty_directory (0.04s) ok 2 tests passed これは、私たちが設計し得た最も短い開発ループです。Go ツールチェーンも、再ビルドも、コンテナの起動も不要です。テストを書き、実行し、繰り返し改善するだけです。 実際のアーティファクトのスキャン プラグインが結果を生成するには、プラグインが探すファイルを含むアーティファクトを inspector-sbomgen に与える必要があります。サンプルプラグインの場合、 example.lock ファイルを含む任意のディレクトリが対象になります。先ほど生成したフィクスチャがちょうど良い題材です。 inspector-sbomgen directory \ --plugin-dir ./my-sbomgen-plugins \ --path ./my-sbomgen-plugins/discovery/cross-platform/extra-ecosystems/my-custom-ecosystem/_testdata \ -o sbom.json --plugin-dir フラグは、Lua プラグインの読み込み元を inspector-sbomgen に伝えます。生成される SBOM には、 example.lock 内の 3 つのパッケージそれぞれに対応する CycloneDX コンポーネントが含まれます。例を以下に示します。 { "bom-ref": "comp-2", "type": "application", "name": "my-package-alpha", "version": "1.0.0", "scope": "optional", "purl": "pkg:generic/my-sbomgen-plugin/my-package-alpha@1.0.0", "properties": [ { "name": "amazon:inspector:sbom_generator:source_path", "value": "./my-sbomgen-plugins/example.lock" } ] } プラグインが生成するすべてのコンポーネントには、収集元のファイルを記録する amazon:inspector:sbom_generator:source_path プロパティが付いています。そのため、コンポーネントを生成元のアーティファクトまで常にたどることができます。 Amazon Inspector による脆弱性スキャン プラグインが生成したパッケージ情報は、他のコンポーネントと同等の正式な SBOM コンポーネントとして扱われます。Amazon Inspector を含め、CycloneDX SBOM を読み取るあらゆる下流のツールで利用できます。SBOM を Amazon Inspector に送信して脆弱性分析を行うには、 --scan-sbom フラグを追加します (有効な AWS アカウントが必要です)。 inspector-sbomgen directory \ --path ./my-sbomgen-plugins/discovery/cross-platform/extra-ecosystems/my-custom-ecosystem/_testdata \ --plugin-dir ./my-sbomgen-plugins \ --scan-sbom \ --aws-profile your_profile \ --aws-region your_region \ -o /tmp/sbom.json まったく新しいエコシステムに対応する際の重要な注意点 : プラグイン作成者は任意のエコシステムをインベントリ化できますが、Amazon Inspector が脆弱性を報告できるのは、アドバイザリが存在するコンポーネントに限られます。アドバイザリフィードにまだ含まれていないエコシステムのコンポーネントを Amazon Inspector に渡すと、Amazon Inspector は Component skipped: no supported rules found (コンポーネントはスキップされました: サポートされるルールが見つかりません) というプロパティ付きでコンポーネントを返します。以下に例を示します。 { "bom-ref": "comp-1", "name": "my-package-alpha", "properties": [ { "name": "amazon:inspector:sbom_scanner:path", "value": "my-sbomgen-plugins/discovery/cross-platform/extra-ecosystems/my-custom-ecosystem/_testdata/example.lock" }, { "name": "amazon:inspector:sbom_scanner:info", "value": "Component skipped: no supported rules found." } ], "purl": "pkg:generic/my-custom-ecosystem/my-package-alpha@1.0.0", "type": "application", "version": "1.0.0" } これはエラーではなく、想定どおりの動作です。SBOM は正しく生成され、コンポーネントは引き続き追跡され、 source_path によってどのファイルから生成されたかを正確に把握できます。Amazon Inspector がそのエコシステムのアドバイザリカバレッジを追加すれば、プラグインを一切変更することなく、同じ SBOM から脆弱性の検出結果が生成されるようになります。Amazon Inspector がすでにサポートしているエコシステムについては、プラグインが生成したコンポーネントは組み込みスキャナーが生成したコンポーネントと区別なく扱われます。 ファーストクラスの IDE サポート AWS は、プラグインを書くときの生産性と効率を重視しています。オートコンプリートのようなモダンな便利機能なしで Lua を書くのは快適とは言えません。そのため、 plugin new コマンドでスキャフォールディングされたすべてのプラグインプロジェクトには、 library/sbomgen.lua 定義ファイルと、それを VS Code の Lua Language Server 拡張機能に自動的に接続する .vscode/settings.json が付属します。 コード補完と IDE サポートを利用するには、まず sumneko.lua 拡張機能をインストールし、VS Code でプラグインプロジェクトを開きます。これにより、すべての sbomgen.* 関数で次の機能が使えるようになります。 型情報付きのパラメータヒント ホバー時のドキュメント表示 定数のオートコンプリート ( sbomgen.component_types.* 、 sbomgen.groups.* 、 sbomgen.platform.* ) 関数呼び出しの型チェック push_package() に必須フィールドが欠けている場合のインライン警告 この定義ファイルのおかげで、AI コーディングアシスタントによるプラグイン開発もうまく機能します。型情報とドキュメントがツールで読み取れる形式で埋め込まれているため、アシスタントは、素の Lua で記述する場合に比べてはるかに少ない人手の確認で正しいプラグインコードを生成できます。 安全な基盤 プラグインは inspector-sbomgen と同じプロセス内で実際のコードを実行するため、そのコードが安定し、セキュリティが強化された状態を保てるように実行環境を設計しました。すべての Lua プラグインは隔離されたサンドボックス内で実行されます。各 Lua 仮想マシン (VM) は、安全な操作のみが許可されるように、Lua 標準ライブラリの制限されたサブセットにのみアクセスできます。 ファイルシステムへの直接アクセスの禁止 – Lua の io ライブラリはロードされません。すべてのファイル操作は sbomgen.* 関数を経由して sbomgen の内部処理にルーティングされるため、ディスク上のディレクトリ、コンテナイメージ、圧縮アーカイブ、マウントされたボリュームのいずれをスキャンする場合でも、プラグインは同じように動作します サブプロセスの実行や環境の変更の禁止 – Lua の os ライブラリはブロックされているため、プラグインはプロセスの起動、環境変数の変更、アーティファクト外のファイルへのアクセスができません VM のイントロスペクションの禁止 – Lua の debug ライブラリはブロックされています 無制限なコードロードの禁止 – dofile 、 loadfile 、 loadstring は削除されています。 require() は利用できますが、プラグイン自身のディレクトリツリーに制限されているため、プラグインは自身のヘルパーモジュールを共有できる一方、他のプラグインやシステムパスからコードをロードすることはできません プラグインが未処理の Lua エラーを発生させた場合、inspector-sbomgen は警告をログに記録し、次のファイルまたはプラグインの処理を続行します。1 つの不具合のあるプラグインが他のプラグインの実行を妨げることはありません。また、プラグインが inspector-sbomgen の組み込みパッケージコレクターを上書きすることもありません。すべてのプラグインは一意の名前を宣言する必要があり、カスタムプラグインが公式の組み込みプラグインですでに使われている名前を使用した場合、そのカスタムプラグインは警告付きでスキップされます。組み込みプラグインが常に優先されるため、カスタムプラグインがツール自身の検出動作をひそかに置き換えたり隠したりすることはできません。 次のステップ 今すぐ独自のプラグインの構築を始めるには、次の手順に従ってください。 Amazon Inspector ユーザーガイド から最新の inspector-sbomgen をインストールします inspector-sbomgen plugin new --with-example を実行し、プロンプトに従います inspector-sbomgen plugin test --path ./my-sbomgen-plugins -v を実行し、サンプルテストがパスすることを確認します サンプルのロジックを、独自のエコシステム向けの検出ロジックに置き換えます すべての関数、定数、コマンドについては、以下の完全なリファレンスドキュメントで詳しく説明しています。 Lua プラグイン開発者ガイド : プラグインの概念、ディレクトリ構造、ライフサイクル Lua プラグインテストガイド : テストフレームワークのリファレンスとフィクスチャの規約 Lua プラグイン API リファレンス : sbomgen.* API の完全なカタログ まとめ 組織独自のロックファイル形式への対応の追加、新しいオープンソースエコシステム向け検出のプロトタイピング、あるいは自作スキャナーから組織全体で大規模に運用できる仕組みへの置き換えなど、どのような用途であっても、このプラグインシステムは、アイデアから動作する SBOM までの道のりをできる限り短くするように設計されています。皆さんがこれを使って何を作るのか、とても楽しみにしています。 この記事に関するご質問がある場合は、 AWS サポートにお問い合わせください 。 Michael Long Michael は AWS の Amazon Inspector 担当 Senior Security Researcher です。Amazon Inspector SBOM Generator と Amazon Inspector for GitHub Actions の研究開発を率いています。AWS 入社前は、MITRE ATT&CK チームで principal adversary emulation engineer を務めていました。また、U.S. Army (米国陸軍) で約 10 年間、軍事情報およびサイバー作戦に従事しました。 Charlie Bacon Charlie は AWS の Amazon Inspector 担当 Head of Security Engineering and Research です。Amazon Inspector や他の Amazon Security の脆弱性管理ツールを支える脆弱性スキャンおよびインベントリ収集サービスを担当するチームを率いています。AWS 入社前は、金融業界とセキュリティ業界で 20 年間にわたり、研究と製品開発の両分野で上級職を務めました。 Anthony Verleysen Anthony は Amazon Inspector 担当の Senior Technical Product Management です。Amazon Inspector の前は、AWS Systems Manager の Product Manager として Node Management 機能を担当していました。仕事以外では、テニスとサッカーに熱心に取り組んでいます。 本ブログは Security Solutions Architect の 中島 章博 が翻訳しました。
本ブログは 2026 年 6 月 11 日に公開された AWS Blog “ AWS Nitro Isolation Engine: Formally verifying the hypervisor in the AWS Nitro System ” を翻訳したものです。 Ali Saidi は AWS の VP 兼 Distinguished Engineer です 何百万ものお客様が、最も機密性の高いワークロードの保護に AWS Nitro System を利用しており、AWS はお客様のデータを保護するイノベーションにおいて業界をリードしています。お客様のデータの安全性と機密性を守ることは AWS の最優先事項であり、データの分離と保護のための専用ハードウェアとソフトウェアへの投資を継続しています。 2017 年、AWS は Nitro System をリリースしました。これは、ゼロオペレーターアクセス (オペレーターがお客様のデータに一切アクセスできない状態) を前提に設計された、初の主要なクラウドプラットフォームです。Nitro System は、次世代のすべての Amazon EC2 インスタンス の基盤となる専用のハードウェアとソフトウェアであり、仮想化、ストレージ、ネットワーキングの機能を専用ハードウェアと最小限のハイパーバイザーにオフロードします。Nitro System では、最も高い権限を持つ AWS オペレーターであっても、認証と監査が行われる管理用 API を通じてのみシステムとやり取りでき、これらの API からお客様のワークロードにアクセスすることはできません。このアーキテクチャはクラウドセキュリティの業界標準を確立し、NCC Group などの第三者機関が独立した立場でこのアプローチの妥当性を確認してきました。 そして今、AWS はさらに水準を高めます。AWS Nitro System の主な役割の 1 つは、インスタンスを互いに分離し、AWS オペレーターからも分離することです。これは 10 年以上にわたり Nitro System アーキテクチャの基盤となってきました。AWS Nitro Isolation Engine は、AWS re:Invent 2025 で初めて発表され、本日 (2026 年 6 月 11 日) よりすべての Graviton5 ベースのインスタンスで一般提供を開始しました。これは Nitro Hypervisor 内の専用コンポーネントであり、この分離を実施し、それを数学的な厳密さで証明する役割を担います。Nitro Isolation Engine は形式的検証を採用しています。形式的検証とは、ハードウェアやソフトウェアが特定のテストケースだけでなく、あらゆる状況で意図したとおりに動作することを数学的に証明する手法です。この徹底的な検証手法により、Nitro は形式的に検証された初のクラウドハイパーバイザーとなり、数学的に証明されたクラウドセキュリティの新しい標準を確立しました。 AWS Nitro Isolation Engine Nitro System において、AWS Nitro Hypervisor は、すべての仮想マシンについて、権限のないエンティティがお客様のデータを読み取ったり変更したりできないように設計されています。Nitro Isolation Engine は Nitro Hypervisor の専用コンポーネントであり、これらの仮想マシン間の分離を実施します。仮想マシンのメモリ、CPU レジスタの状態、および I/O デバイスへのすべてのアクセスを、Nitro Hypervisor の他の部分に公開される最小限の API セットを通じて仲介します。Nitro Isolation Engine は、お客様のデータへのアクセスを仲介する唯一のシステムコンポーネントです。Nitro Hypervisor の他のコンポーネントは、この制限されたインターフェイスを通じて動作する必要があり、お客様のワークロードに直接アクセスすることはできません。Nitro Isolation Engine のコードベースは最小限に抑えられているため、人による監査が容易になり、バグが混入し得る範囲が減り、その設計と実装に形式的検証を適用することが現実的になっています。 形式的検証 形式的検証は、数学的証明を用いて、システムの形式的モデルの特性が、あり得るすべてのシステム状態とすべての入力において成立することを示します。これは、あり得る状態と入力のうち (場合によっては大規模な) 一部に対してシステムの動作を確認するテストとは対照的です。形式的検証は、従来のテストよりも正確性についてはるかに強い根拠を提供します。Nitro Isolation Engine の場合、分離特性は、あり得るすべてのシステムの動作にわたって保証されます。テストと検証は相互補完的な関係にあります。検証はテストを補強し、テストはまだ検証されていないシステムの領域をカバーして、システムが意図したとおりに動作しているという経験的な裏付けを与えます。 お客様にとって、分離を実施するコードが形式的に検証されていることは、包括的なテストを超える保証となります。テストは引き続き不可欠であり、AWS はテストにおいても高い基準を維持していますが、テストで確認できるのは特定のシナリオに限られます。形式的検証はこれを補完するものであり、テストでカバーされるシナリオだけでなく、あり得るすべてのシナリオにわたって分離特性が数学的に保証されることを意味します。 形式的に検証された特性 Nitro Isolation Engine の形式的検証により、4 つの主要な特性が確立されます。 1. 機密性と完全性 – Nitro Isolation Engine は、ゲスト仮想マシン (VM) の機密性と完全性を維持します。機密性とは、ゲスト VM のプライベートデータが権限のないエンティティによって読み取られないことを意味し、完全性とは、ゲスト VM のプライベートデータが権限のないエンティティによって変更されないことを意味します。 2. 機能的正確性 – 検証されたすべてのハイパーコールは、仕様で定義された期待される動作と一致します。仕様には各ハイパーコールの事前条件と事後条件が記述されており、証明によって、実装がそれらから決して逸脱しないことが立証されます。 3. ランタイムエラーが発生しないこと – コードがランタイムエラーに遭遇することはなく、実装は仕様どおりに動作します。これらの特性の形式的検証を組み合わせることで、検証の対象となるあらゆる一連のイベントに対して Nitro System が分離を維持することが、数学的に厳密に保証されます。現時点では、この検証は、VM の起動、実行、および終了を担う VM のコアライフサイクルのハイパーコールを対象としています。 4. メモリ安全性 – バッファオーバーフロー、NULL ポインタ逆参照、範囲外アクセスなどのメモリ安全性違反が存在しないことを立証します。すべての検証済みソフトウェアと同様に、Nitro Isolation Engine の証明は、Rust コンパイラやハードウェアの正確性などの前提に基づいています。これらの前提、およびエンジニアリングと検証に対する AWS のアプローチについては、Nitro Isolation Engine のホワイトペーパーで詳しく説明されています。 Rust による実装 Nitro Isolation Engine は Rust で実装されています。Rust は、機密性の高いソフトウェアにおけるセキュリティ脆弱性の根本原因となってきた、よくあるプログラミング上の落とし穴を防ぐように設計されたシステムプログラミング言語です。Nitro Isolation Engine に Rust を採用したことで、複数の種類のバグを設計上まとめて排除しています。Rust が適している理由はその型システムにあります。型システムが厳格な所有権規則を適用するため、形式的検証の一部が容易になり、コンパイル時に第一段階の保証が得られます。 まとめ Nitro Isolation Engine は、お客様のデータの機密性を守るという AWS の継続的なコミットメントを体現するものです。そして、これはまだ出発点にすぎません。AWS は、セキュリティに影響を与える Nitro Isolation Engine のすべての主要コンポーネントに形式的検証を拡張し、新機能が導入されてもそれらの証明を維持し続けます。さらに、Nitro Isolation Engine のソースコードと形式的証明を第三者に提供し、独立した検査およびレビューを受けられるようにする予定です。このレベルの透明性は、クラウドプロバイダーが開かれた姿勢、コード品質、形式的検証をどのように示せるかについて、新しい標準を確立するものと考えています。 AWS Nitro System とコンフィデンシャルコンピューティングの詳細については、以下のリソースをご覧ください。 AWS Nitro Isolation Engine Whitepaper コンフィデンシャルコンピューティング: AWS の視点 (2021) AWS Nitro System のコンフィデンシャルコンピューティングに対する第三者評価の獲得 (2023) AWS Nitro Whitepaper AWS re:Invent 2025 presentation – Introducing Nitro Isolation Engine: Transparency through Mathematics 著者について Ali Saidi Ali は Amazon Web Services (AWS) のバイスプレジデント兼 Distinguished Engineer です。ミシガン大学でコンピュータサイエンスおよび工学の博士号を取得しています。2017 年に AWS に入社して以来、AWS Nitro System、AWS Graviton、および幅広い EC2 インスタンスファミリーのポートフォリオの設計と開発に注力しています。 本ブログは Security Solutions Architect の 中島 章博 が翻訳しました。

動画

書籍