Solaris 8 のソフトウェア開発 (追補)

ドライバの強化

強化 (hardening) とは、ドライバが制御する入出力デバイスで障害が発生しても、またはシステムコアの外部に起因するその他の障害が発生しても、そのドライバが正常に動作するようにするプロセスです。強化されたドライバは、パニックに陥ったり、システムをハングさせたり、またこのような障害の結果として壊れたデータを無秩序に拡散させたりしてはなりません。

ドライバの開発者は、次の役割を担います。

すべての Solaris ドライバは強化されるべきです。強化されたドライバは、次の規則を守ります。

デバイスドライバのインスタンス

Solaris カーネルは、複数のドライバのインスタンスの使用を認めます。インスタンスごとに専用のデータ領域を持ちますが、テキストおよび一部のグローバルデータは、他のインスタンスとの共有になります。デバイスはインスタンス単位で管理されます。強化されたドライバは、ドライバがフェイルオーバーを内部的に処理する設計になっていない限り、ハードウェアの部分ごとに別々のインスタンスを使用すべきです。たとえば、多機能カードなど、スロットごとに複数のドライバインスタンスが存在することになりますが、これは Solaris デバイスドライバの標準の動作です。

DDI アクセスハンドルの排他的使用

強化されたドライバによるすべてのプログラムされた入出力 (PIO) アクセスでは、必ず、ddi_getXddi_putXddi_rep_getX、および ddi_rep_putX ルーチンファミリーに含まれる Solaris DDI アクセス関数を使用しなければなりません。ドライバが、ddi_regs_map_setup(9F) から返されたアドレスを使用して、マップされたレジスタに直接アクセスしてはなりません。アクセスハンドルを使用することによって、入出力エラーが確実に制御され、その影響が戻り値に限定されるので、マシン状態の他の部分を壊す可能性が減少します (アクセスハンドルを使用しないため、ddi_peek(9F) および ddi_poke(9F) ルーチンは避けてください)。

DDI アクセスメカニズムは、カーネルにデータを読み込む方法を制御する機会を提供するので重要です。DDI アクセスルーチンは、バスタイムアウトトラップの影響を限定することによって、保護能力を提供します。

破壊されたデータの検出

以下の節では、どこでデータ破壊が発生する可能性があるか、およびそれらを検出するにはどのような手順が必要かについて説明します。

デバイス管理および管理データの破壊

ドライバは、PIO によるか DMA によるかを問わず、デバイスから取得したデータは壊れている可能性があるということを想定しなければなりません。特に、デバイスから得たデータから読み取った、または計算されたポインタ、メモリーオフセット、配列インデックスは、慎重に扱ってください。この種の値は有害な場合があり、参照に利用される場合、カーネルパニックを引き起こす可能性があります。この種の値はすべて、範囲および整列 (必要な場合) をチェックしてから使用してください。

ポインタが有害でなくても、誤った結果を導く可能性もあります。たとえば、オブジェクトの有効なインスタンスを指し示していても、該当する正しいインスタンスではないことがあります。可能であれば、ドライバはポインタと指示先オブジェクトをクロスチェックするか、またはポインタによって取得したデータの妥当性を検証すべきです。

他のタイプのデータ (パケット長、ステータスワード、チャネル ID など) も誤った結果を導く可能性があります。それぞれを可能な範囲でチェックすべきです。たとえば、パケット長の場合は、範囲をチェックして、負ではないか、収容バッファより大きくないかどうかを確認できます。また、ステータスワードの場合は、「不可能」ビットの有無をチェックできます。チャネル ID は有効 ID のリストと突き合わせます。

値を使用して Stream を識別する場合、ドライバは Stream が実際に存在しているかどうかを確認しなければなりません。STREAMS 処理は非同期の性質があるので、デバイスの割り込みがまだ処理されないうちに、Stream が取り除かれてしまうことがあります。

ドライバはデバイスからデータを再読み込みしてはなりません。データは 1 回だけ読み取って、妥当性を検証し、ドライバのローカル状態で格納します。こうすることによって、最初に読み取って検証したときには正しかったが、後で再読み取りしたときには正しくないというデータによる危険性を回避することができます。

さらに、連続して BUSY 状態を返すデバイス、または別のバッファ処理を要求するデバイスがシステム全体をロックアップすることがないように、ドライバで、すべてのループがバインドされていることも確認すべきです。

受信したデータの破壊

デバイスエラーによって、受信バッファに壊れたデータが格納されてしまうことがあります。このような破壊を、デバイスドメインの外 (たとえば、ネットワーク内など) で発生した破壊と区別することはできません。通常は、プロトコルスタックのトランスポートレイヤーやデバイスを使用するアプリケーション内部での整合性チェックなど、この種の破壊に対処する既存のソフトウェアが既に配備されています。

ディスクドライバなどのように、受信したデータの整合性を上位レイヤーでチェックしない場合は、ドライバ内部で整合性をチェックすることができます。受信したデータの破壊を検出する方法は、一般にデバイス固有です (チェックサム、CRC など)。

障害の検出

従来のデバイスドライバでは、障害を検出した場合に、デバイスまでのデータパスを無効にできます。PIO アクセスが無効にされている場合にそのデバイスから読み取りを行おうとすると、undefined 値が返り、書き込みが無視されます。DMA アクセスが無効にされている場合は、デバイスはメモリーへのアクセスが禁止される可能性があり、または読み取り時に未定義データを受け取り、書き込みが廃棄される可能性があります。

デバイスドライバは次の DDI ルーチンを使用することによって、データパスが無効にされているかどうかを検出することができます。

各関数は、指定のハンドルによって示されたデータパスに影響を与えるような障害が検出されているかどうかを調べます。いずれかの関数が DDI_FAILURE を返した場合、そのデータパスに障害があります。ドライバは ddi_dev_report_fault(9F) を使用してその障害を報告し、必要なクリーンアップを実行し、さらに可能であれば、呼出側に適切なエラーを返す必要があります。

障害の封じ込め

システムの整合性を維持するには、システム状態が変更されないうちに障害を検出する必要があります。したがって、ドライバは、システムが使用する予定のデータがデバイスから返されるたびに、障害の有無をテストしなければなりません。

DMA の切り離し

障害のあるデバイスは、バス上で不適切な DMA 転送を開始する可能性があります。このデータ転送によって、配信済みの正常データが破壊されることがあります。障害のあるデバイスは、そのデバイスのドライバに属さないメモリーにまで悪影響をおよぼすような破壊されたアドレスを生成しかねません。

IOMMU が備わったシステムでは、デバイスは DMA 用に書き込み可能としてマップされたページに限って書き込むことができます。したがって、DMA 書き込みのターゲットとなるページは、1 つのドライバインスタンスが単独で所有し、他のカーネル構造体と共有することはありません。該当するページが DMA 用に書き込み可能としてマップされている場合、ドライバはそのページのデータを疑ってみるべきです。ページをドライバの外部に渡す前に、またはデータを検証する前に、ページと IOMMU のマッピングを解除しなければなりません。

ddi_umem_alloc(9F) を使用して、整合ページ全体が割り当てられる、または複数のページを割り当てて最初のページ境界より下のメモリーを無視することができます。ddi_ptob(9F) を使用すると、IOMMU ページのサイズを調べることができます。

あるいは、ドライバは、メモリーの安全な部分にデータをコピーしてから、そのデータを処理することもできます。この場合、最初に ddi_dma_sync(9F) を使用してデータを同期させる必要があります。

ddi_dma_sync(9F) を呼び出すときには、DMA を使用してデバイスにデータを転送する前に SYNC_FOR_DEV を指定し、デバイスからメモリーへ DMA を使用してデータを転送した後で SYNC_FOR_CPU を指定する必要があります。

IOMMU が備わった一部の PCI ベースのシステムでは、デバイスは PCI デュアルアドレスサイクル (64 ビットアドレス) を使用して、IOMMU をバイパスすることができます。その結果、デバイスにおいてメインメモリーの領域が破壊されてしまう潜在的可能性が生じます。強化されたデバイスドライバでは、このようなモードを使用しようとしてはならず、使用不可にしておくべきです。

stuck 割り込みの処理

割り込みが絶えないと、システムパフォーマンスが大幅に低下し、シングルプロセッサマシンの場合はほぼ確実に作業が進まなくなってしまうので、ドライバは stuck 割り込みを識別する必要があります。

時にはドライバにとって、特定の割り込みを偽として識別するのが困難な場合があります。ネットワークドライバの場合、受信した割り込みが指示されても、新しいバッファが利用できなければ作業は不要です。これが単独で発生した場合は問題ありません。実際の作業は別のルーチン (たとえば読み取りサービスなど) によってすでに完了している可能性があるからです。

一方、ドライバが処理する作業を伴わない割り込みが連続した場合、stuck 割り込みラインを示していることがあります。そのため、防御手段を講じる前に、すべてのプラットフォームで多数の明らかに偽の割り込みの発生を許してしまうことになります。

処理すべき作業がありそうなのにハングしてしまったデバイスは、対応するバッファ記述子を更新できなかった可能性があります。ドライバは、このような繰り返しの要求に対して防御しなければなりません。

場合によっては、プラットフォーム固有のバスドライバに請求によらない持続的な割り込みを識別する能力があり、攻撃しているデバイスを使用禁止にできます。しかし、これは有効な割り込みを識別して適切な値を返すことができるという、ドライバの能力に依存します。ドライバはしたがって、デバイスが正当な割り込みをかけた (すなわち、デバイスがドライバに、なんらかの有用な作業を行なうことを実際に要求している) ことを検出した場合以外は、DDI_INTR_UNCLAIMED を返さなければなりません。

偶発性の高い他の割り込みの正当性を確認することは、さらに困難です。この目的のためには、割り込みが有効かどうかを評価する手段として、割り込み想定フラグが役立ちます。デバイスの記述子が全部すでに割り当てられている場合に生成できる、descriptor free などの割り込みを例として仮定してください。ドライバはカードの最後の記述子を使用してしまったことを検出すると、割り込み想定フラグを設定できます。対応する割り込みが伝達された時にこのフラグが設定されていないと、疑わしいといえます。

メディアが切断された、またはフレーム同期が失われたといったことを伝える、情報通知目的の割り込みには、予測できないものがあります。このような割り込みが stuck かどうかを検出する最も簡単な方法は、最初のイベントでその送信元を次のポーリングサイクルまでマスクすることです。

禁止されている間に割り込みが再び発生した場合は、偽の割り込みとみなすべきです。デバイスによっては、マスクレジスタが対応する送信元を無効にし、割り込みを発生させない場合でも読み取ることのできる、割り込み状態ビットがあります。ドライバの設計者は、それぞれのデバイスに合わせて、より適切なアルゴリズムを工夫することができます。

割り込み状態ビットが無限ループに陥らないようにしてください。パスの最初に設定された状態ビットセットがいずれも実際の作業を必要としない場合は、このようなループを切断してください。

ドライバの強化に関するその他の考慮事項

すでに説明した要件のほかに、ドライバの開発者は、次の問題も考慮する必要があります。

スレッドのインタラクション

デバイスドライバのカーネルパニックは、デバイス障害の発生後に生じた予想外のカーネルスレッドのインタラクションによって引き起こされることがよくあります。デバイスで障害が発生すると、設計者が予想しなかった形でスレッドのインタラクションが起きることがあります。

たとえば、処理ルーチンが正常に完了しないうちに打ち切られた場合、条件変数を待機している他のスレッドに知らせることができないことがあります。他のモジュールに障害を知らせようとしたり、または予想外のコールバックを処理しようとしたりすると、望ましくないスレッドのインタラクションが発生する可能性があります。デバイス障害時に発生する可能性のある、相互排他ロックの取得と放棄の順序を検証してください。

アップストリームの STREAMS モジュールを起点とするスレッドは、予想外にそのモジュールをコールバックするために使用された場合、望ましくないパラドックスに陥る可能性があります。代替スレッドを使用して、例外メッセージを処理してください。たとえば、wput 手順では、読み取り側の putnext で直接通信するのではなく、読み取り側のサービスルーチンを使用して M_ERROR と通信することができます。

障害の発生した STREAMS デバイスが、クローズ時に (障害が原因で) 静止できなかった場合、Stream を取り除いた後に割り込みが発生する可能性があります。割り込みハンドラは、古い Stream ポインタを使用して、メッセージを処理しようとしてはなりません。

トップダウン式要求における危険

ドライバの設計者は、ハードウェアの故障からシステムを保護する一方で、ドライバの誤用に対しても防御する必要があります。ドライバは、カーネル基盤は常に正しい (信頼できるコア) ということを前提にできますが、ドライバに渡されるユーザー要求は潜在的な破壊性を有している可能性があります。

たとえば、ユーザーが提供したデータブロック (M_IOCTL) に対してアクションを実行することをユーザーが要求し、そのデータブロックがメッセージの制御部で指示されたサイズより小さいという場合があります。ドライバはユーザーアプリケーションを信用してはなりません。

設計時には、害を引き起こす可能性があるという観点から、受け取ることのできる各タイプの ioctl の構造を検討すべきです。ドライバは、異常な ioctl を処理しないように、チェックしなければなりません。

代替の対応策

ドライバは障害の起きたハードウェアを引き続き使用してサービスを提供し、代替のデバイスアクセス方法を使用することによって、特定された問題への対処を試みることができます。ハードウェアの故障が予測不能で、設計が複雑になるリスクを伴うことを考えると、代替の対応策をとることが常に賢明な選択というわけではありません。せいぜい、周期的な割り込みポーリングと再試行に限定すべきです。周期的にデバイスを再試行することによって、ドライバはデバイスが復旧したかどうかを知ることができます。周期的ポーリングを使用すると、ドライバが割り込みを強制的に禁止された後でも、割り込みメカニズムを制御することができます。

重要なシステムサービスを提供できるように、システムが常に代替デバイスを備えているのが理想です。カーネルまたはユーザースペースのサービスマルチプレクサは、デバイス障害時にシステムのサービスを維持する最良の手段です。しかし、この章ではこの種の方式については記載していません。