代表的な面接トピック

Linux 面接:ハードリンク vs シンボリックリンク

一般普通
Offer.cc 編集チーム公開日 更新日

質問

Linux デプロイメントにおいて、同一の設定ファイルに対してハードリンクと相対シンボリックリンクを 1 つずつ作成し、1 つのプロセスがそのファイルをオープンしたままの状態でパスの名前変更と削除を行います。どのパスとディスクリプタが引き続き機能するかを予測し、ハードリンクがファイルシステムを跨げずシンボリックリンクが跨げる理由を説明し、各主張をどのように検証するかを示してください。

プロンプトと適用されるコンテキスト

あるサービスが /srv/releases/v1/app.conf をデプロイします。このサービスは /srv/live/pinned.conf をハードリンクとして作成し、/srv/live/current.conf を相対シンボリックリンクとして作成します。オペレータがディレクトリエントリの名前を変更し、後に削除する前に、ワーカープロセスがこのリリースファイルをオープンします。

ファイル名、ハードリンク、シンボリックリンク、inode、オープンファイルディスクリプタがそれぞれ何を指すかを説明してください。各操作後の結果を予測してください。その後、イミュータブルなスナップショット、移動可能なリリースポインタ、ファイルシステムを跨ぐ参照に対して適切なメカニズムを選択してください。回答は Linux システムおよび通常のファイルを対象とします。ファイルシステム固有のスナップショットや Windows のショートカットは対象外です。

面接官が評価するポイント

第一の評価基準はオブジェクトモデルです。ディレクトリエントリは 1 つの名前を 1 つの inode にバインドします。ハードリンクは同じ inode に対する別のディレクトリエントリであり、どちらの名前も「オリジナル」ではありません。シンボリックリンクは別のファイルシステムオブジェクトであり、そのペイロードは使用時に解決されるパス名です。

第二の評価基準はライフサイクルの推論です。unlink は 1 つのディレクトリエントリを削除しますが、必ずしもファイルオブジェクト自体を削除するわけではありません。別のハードリンクまたはオープン中のファイル記述(open file description)がそれらを参照し続けている限り、inode とデータは残ります。シンボリックリンクは、格納されたパスが解決できなくなっても存在し続けることができます。

第三の評価基準は、比較表を暗記して暗唱するのではなく、操作結果を正確に予測できることです。1 つのハードリンク名を変更しても、そのピア(他のハードリンク)には影響しません。シンボリックリンクの名前変更は、シンボリックリンク自体に対して操作が行われます。シンボリックリンクまたはそのターゲットを移動すると、相対パスの意味が変わる可能性があります。inode の同一性は単一のファイルシステム内でのみローカルであるため、ファイルシステムを跨ぐハードリンクは失敗します。

最後の評価基準は運用上の判断力です。リンクの同一性とカウントを検査し、statlstat を区別し、必要に応じて同一ファイルシステム内でのアトミックな置換を実行し、攻撃者に制御されるパスにおけるチェック後のオープン(check-then-open)の競合を回避できるかを確認します。

回答前に明確にすべき質問

  • ソースと新しいハードリンク名は同一のマウントされたファイルシステム上にありますか? そうでない場合、linkEXDEV で失敗します。データをコピーするかシンボリックリンクを使用するかは、異なる問題を解決します。
  • 呼び出し元は安定したオブジェクトを必要としていますか、それとも移動可能な名前を必要としていますか? ハードリンクは特定の inode を固定します。リリースエイリアスは通常、ディレクトリエントリを置換できるシンボリックリンクであるべきです。
  • ターゲットは通常ファイルですか、それともディレクトリですか? Linux は通常、ディレクトリへのハードリンクを禁止していますが、シンボリックリンクはいずれの名前付けにも使用できます。
  • シンボリックリンクは絶対パスですか、相対パスですか? 相対ターゲットは、プロセスの現在の作業ディレクトリからではなく、シンボリックリンクを含むディレクトリから解決されます。
  • ファイルを開いているプロセスは既に存在しますか? パス名の変更やリンク解除(unlink)を行っても、既存のディスクリプタはリダイレクトされません。そのプロセスはオープン済みのオブジェクトを使い続けます。
  • 切り替えは読み取り元に対してアトミックである必要がありますか? rename による置換は、同一のマウントされたファイルシステム内でのみアトミックです。ファイルシステムを跨ぐ移動には別の公開プロトコルが必要です。
  • パスの構成要素は信頼できないユーザーによって制御されていますか? その場合、事前の lstat に続けて open を実行すると競合が発生します。オープン処理自体でトラバーサルポリシーを強制する必要があります。

30秒の回答フレームワーク

「ハードリンクは同じ inode に対する 2 つ目のディレクトリエントリであるため、両方の名前でデータとメタデータが共有されます。シンボリックリンクは独自の inode を持ち、アクセス時に解決されるパスを格納します。ハードリンク名を 1 つ削除するとリンクカウントが減算されますが、他のハードリンクやオープン中のディスクリプタを通じてオブジェクトは維持されます。ターゲットを削除または名前変更すると、シンボリックリンクがダングリング(参照切れ)になる可能性があります。

私は ls -listatlstat または readlink、およびオープン済みディスクリプタを使用して検証します。単一のファイルシステム内で正確なファイルを固定するにはハードリンクを、置換可能なリリースエイリアスやファイルシステムを跨ぐ参照にはシンボリックリンクを、アトミックなエイリアス切り替えには一時的なシンボリックリンクと同一ファイルシステム内での rename を使用します。信頼できないパスには、個別のチェックではなく、アトミックなシンボリックリンク禁止またはディレクトリ配下限定のオープンポリシーが必要です。」

ステップバイステップの詳細解説

ステップ 1: 名前からオブジェクトへのモデルを構築する

通常ファイルの場合、ディレクトリは名前と inode 参照を格納します。inode はファイルタイプ、所有権、モード、タイムスタンプ、サイズ、ブロックマッピング、およびハードリンクカウントを保持します。ファイルの内容は、特権的な 1 つの「元のファイル名」に属しているわけではありません。

ハードリンクは、その inode に別のディレクトリエントリを追加します。いずれかの名前を通じてバイトデータやモードを変更すると、もう一方の名前を通じても確認できます。対照的に、シンボリックリンクは独自の inode を持ち、../releases/v1/app.conf のような文字列を格納します。通常のパス名ルックアップは、その文字列を別の名前へと辿ります。

ステップ 2: 具体的な実験を実行する

次のコマンドはシナリオを再現します。シェルのファイルディスクリプタは意図的に開いたままに維持されます。

bash
mkdir -p /srv/releases/v1 /srv/live
printf 'version=1\n' > /srv/releases/v1/app.conf
ln /srv/releases/v1/app.conf /srv/live/pinned.conf
ln -s ../releases/v1/app.conf /srv/live/current.conf
exec 3< /srv/releases/v1/app.conf

ls -li /srv/releases/v1/app.conf /srv/live/pinned.conf
readlink /srv/live/current.conf
stat -L -c '%F %i %h' /srv/live/current.conf
stat -c '%F %i %h' /srv/live/current.conf

mv /srv/releases/v1/app.conf /srv/releases/v1/app.conf.moved
cat /srv/live/pinned.conf
cat /srv/live/current.conf
cat <&3

rm /srv/releases/v1/app.conf.moved /srv/live/pinned.conf
cat <&3
exec 3<&-

名前変更の前には、リリースパスと pinned.conf は同じ inode と 2 のハードリンクカウントを示します。readlink はシンボリックリンクのペイロードを出力します。通常の stat はシンボリックリンクをターゲットまで辿りますが、例のリンクを辿らない検査ではシンボリックリンクオブジェクト自体が報告されます。

ステップ 3: 名前変更の動作を予測する

このファイルシステム内での mv は、ソースエントリに対する名前変更操作を使用します。inode は移動しないため、pinned.conf とディスクリプタ 3 は引き続き機能します。current.conf は依然として ../releases/v1/app.conf を格納しています。その古い名前が消えたため、参照解決は失敗するようになります。シンボリックリンク自体の名前を変更した場合は、ターゲットではなくそのリンクオブジェクトが移動し、解決がリンクの新しい格納ディレクトリから開始されるため、相対ペイロードの意味が変わる可能性があります。

デプロイメントで名前が存在しない期間を作らずに current を切り替える必要がある場合は、同じディレクトリ内に完全に準備された一時的なシンボリックリンクを作成し、currentrename で置換します。置換後にオープンするリーダーは、古いディレクトリエントリか新しいディレクトリエントリのいずれかを解決します。古いターゲットを既に開いているプロセスは、古いディスクリプタを保持し続けます。

ステップ 4: リンク解除(unlink)と領域回収を予測する

app.conf.moved を削除すると、ハードリンクが 1 つ減少します。pinned.conf は依然としてその inode を指しているため、そのデータは残ります。pinned.conf を削除すると最後のディレクトリエントリが削除されますが、ディスクリプタ 3 がまだオープン参照を保持しているため、cat <&3 はファイルの内容を読み取り続けます。ストレージが回収可能になるのは、最後のハードリンクと最後のオープン参照の両方が消えた後のみです。

これは、アクティブな大きなログファイルを削除してもディスク容量が解放されない理由を説明していますが、任意のディスクリプタを切り詰める(truncate)理由にはなりません。まず所有プロセスとそのローテーション規約を特定し、必要に応じて安全にシグナルを送信するか再起動してください。

ステップ 5: ファイルシステムとディレクトリの境界を適用する

inode 番号は、そのファイルシステム内でのみ一意です。ハードリンクは別のファイルシステムからその inode を指定することはできず、linkEXDEV を返します。シンボリックリンクは、アクセス時に名前解決が行われるため、マウント境界を跨ぐパスを格納できます。また、ディレクトリやまだ存在しないターゲットを指定することもできます。

Linux は、循環参照やトラバーサルの曖昧さを防ぐために、ディレクトリへの通常のハードリンクを禁止しています。ディレクトリへのシンボリックリンクは許可されますが、相対リンクを移動すると壊れる可能性があり、再帰的ツールによって追跡ポリシーが異なる場合があります。バックアップ、削除、およびデプロイメントツールに対してこれらのポリシーを明記してください。

ステップ 6: 不変条件によって選択する

不変条件が「この追加の名前によって正確な inode を到達可能に保たなければならない」、オブジェクトが同一ファイルシステムを共有している、および共有メタデータが意図されている場合は、ハードリンクを使用します。不変条件が「このエイリアスを現在格納されているパスに解決する」である場合、特にディレクトリ、リリースポインタ、またはファイルシステムを跨ぐ名前にはシンボリックリンクを使用します。独立したバイトデータ、メタデータ、保持期間、またはファイルシステム配置が必要な場合は、コピーを使用します。

どちらのリンクも、それ単体ではバックアップにはなりません。ハードリンクされた名前は変更や破損を共有します。シンボリックリンクにはターゲットデータが含まれていません。バックアップには、独立した障害境界と保持境界、およびリストアテストが必要です。

ステップ 7: 同一性と安全なトラバーサルを検証する

inode 番号は異なるファイルシステム間で重複する可能性があるため、inode 単体ではなくデバイスと inode を比較してください。stat でハードリンクカウントを確認し、lstat スタイルの動作でシンボリックリンク自体を検査し、readlink でそのペイロードを出力し、ディスクリプタを開いた状態で正確な rename/unlink シーケンスをテストします。

信頼できないパスの場合、lstat(path) の後に open(path) を実行すると、呼び出しの合間に攻撃者がコンポーネントを差し替える可能性があります。Linux では、信頼できるディレクトリディスクリプタを基準としてオープンし、シンボリックリンクの禁止やディレクトリより上位へのエスケープ禁止など、必要な openat2 解決制限を適用します。セキュリティは、ディスクリプタを返すルックアップ処理によって強制されなければなりません。

優れた回答例

「ディレクトリエントリから説明します。app.confpinned.conf は 1 つの inode に対する 2 つの同等な名前であるため、コンテンツ、パーミッション、所有権、タイムスタンプを共有します。current.conf は、ペイロードが ../releases/v1/app.conf である独立したシンボリックリンク inode です。相対パスは、シンボリックリンクが存在する /srv/live から開始されます。

リリースパスの名前を変更した後も、ハードリンクと既にオープンされているディスクリプタは同じファイルオブジェクトを参照し続けます。シンボリックリンクは、格納されたパス名が古いエントリを指したままであるため、ダングリングになります。移動された名前を削除した後も、ハードリンクによってオブジェクトは維持されます。ハードリンクも削除した後でも、オープン中のディスクリプタは機能し続けます。カーネルがオブジェクトを回収できるのは、そのディスクリプタがクローズされたときだけです。

同一ファイルシステム内の正確な成果物を固定するにはハードリンクを、移動可能な current エイリアスにはシンボリックリンクを、独立した保持には実コピーを使用します。current を更新する際は、一時的なシンボリックリンクを作成し、同じディレクトリ内でアトミックに名前変更します。デバイス/inode、リンクカウント、readlink、ダングリングリンクへのアクセス、およびオープンディスクリプタのテストによって結果を証明します。パスが信頼できない場合は、オープン処理自体の中でシンボリックリンク禁止およびディレクトリ境界のルールを強制します。」

よくある間違い

  • ハードリンクを元の名前へのポインタと呼ぶ → すべてのハードリンクは 1 つの inode に対する対等なディレクトリエントリです → 名前、inode の同一性、リンクカウントを説明してください。
  • 削除によって常にファイルが直ちに破棄されると述べる → 他の名前やオープン参照が残っている可能性があります → ハードリンクカウントとオープンディスクリプタの両方を追跡してください。
  • シンボリックリンクを格納された inode 参照として扱う → 後で異なる解決がされたり失敗したりする可能性があるパス名を格納しています → ペイロードと解決の基準を検査してください。
  • デバイスの同一性なしに inode 番号を比較する → inode 番号はファイルシステム内でのみローカルです → デバイスと inode の両方を比較してください。
  • マウントを跨いだりディレクトリに対してハードリンクを使用する → Linux はそれらのケースを拒否します → 必要な独立性に応じてシンボリックリンクまたはコピーを使用してください。
  • 相対シンボリックリンクを再評価せずに移動する → それが含まれるディレクトリが基準を定義します → 再配置後にターゲットを再計算または再生成してください。
  • いずれかのリンクをバックアップと呼ぶ → 一方はオブジェクトを共有し、もう一方はパスのみを格納します → 独立してリストア可能なコピーを作成してください。
  • パスをチェックしてから後でオープンする → 攻撃者が操作の間にシンボリックリンクを入れ替える可能性があります → オープン時にアトミックにトラバーサル制約を強制してください。

フォローアップの質問と回答

フォローアップ 1: ハードリンクには個別のパーミッションや所有者がありますか?

いいえ。パーミッション、所有者、サイズ、タイムスタンプは共有された inode に属します。1 つのハードリンクを通じてそれらを変更すると、すべてのピアから見える内容が変更されます。ディレクトリエントリ名とその親ディレクトリのパーミッションは別個のものです。名前の変更やリンク解除は、それを含むディレクトリによって制御されます。

フォローアップ 2: 相対シンボリックリンクが移動後に壊れることがあるのはなぜですか?

そのペイロードは、シンボリックリンクを含むディレクトリからの相対パスとして解釈されるためです。リンクを移動すると、格納された文字列を保持したままその基準が変更されます。絶対シンボリックリンクは 1 つのパス名を維持しますが、コンテナ、chroot、別のマウント、または別のホスト内では不正になる可能性があります。再配置の要件を定義した上で選択してください。

フォローアップ 3: rename はファイルシステムを跨いで current をアトミックに置換できますか?

いいえ。Linux の rename は、マウントされたファイルシステムを跨ぐ場合に EXDEV を返します。完了したターゲットを宛先ファイルシステム内に公開し、そこに一時エイリアスを作成し、同じディレクトリ内でそのエイリアスを current にリネームしてください。以前のコピーフェーズに対するクリーンアップとリカバリを定義してください。

フォローアップ 4: 最後のパス名が削除された後もディスク容量が使用されたままになるのはなぜですか?

オープンファイル記述がまだ inode を参照している可能性があります。所有しているディスクリプタとプロセスを特定し、アプリケーションの安全なローテーションまたは再起動手順を使用してください。最後の名前と最後のオープン参照の両方が消えた後に容量が回収されます。パス名を削除しただけでは、その名前が消えたことしか証明されません。

フォローアップ 5: statlstatreadlink はどのように異なりますか?

stat は通常、最後のシンボリックリンクを辿ってターゲットを報告します。lstat はシンボリックリンクオブジェクト自体を報告します。readlink は解決を行わずに格納されたパス名を返します。エイリアスとそれが現在到達するオブジェクトの両方を証明する際には、これら 3 つの概念すべてを使用してください。

フォローアップ 6: アップロードディレクトリでのシンボリックリンクの競合をどのように防止しますか?

信頼できるディレクトリディスクリプタを基準としてオープンし、ルックアップ処理がそのディレクトリ配下にとどまり、禁止されたシンボリックリンクを辿らないように強制します。その後、返されたディスクリプタをディスクリプタベースの操作で検証します。パス名のチェックの後に個別のオープンを行うと、差し替えの隙間(タイムウィンドウ)が生じます。

公開情報ソース

関連する質問