memfdのシール、内容の固定とサイズの固定を分けて考える

薄紫の薄い板を重ね、上に青緑の丸い封印を置き、左右を白い留め具で支えている。

共有するデータを「確定したので、もう変えられない状態」にしたい。ただし、内容の書き換えを止めることと、入れ物の大きさを変えられなくすることは同じではありません。Linuxのmemfdで利用できるファイルシールは、変更の種類ごとに制約を加える仕組みです。

ここではmemfd_createとfcntlのシール操作を対象に、内容、縮小、拡大、シールの追加という四つの違いを整理します。名前から「全て固定された」と判断せず、何が禁止され、何が残るかを読み取るための説明です。

簡単にまとめると?

  • F_SEAL_WRITEは内容の変更を制限しますが、ファイルサイズの縮小・拡大は別のシールで制限します。
  • 追加したシールは外せません。F_SEAL_SEALはシール集合を固定するためのもので、単独で内容やサイズを固定する意味ではありません。
  • F_SEAL_FUTURE_WRITEでは、設定前から存在する共有・書込み可能なマッピングによる変更が残ります。完全に確定した内容を渡す場合とは区別します。

memfdは、名前で共有する普通のファイルとは違う

memfd_createは匿名ファイルを作り、それを参照するファイル記述子を返します。作成直後のサイズは0で、ftruncateで大きさを設定したり、writeで内容を入れたりできます。揮発的な記憶を使い、参照が全て失われると解放されます。名前の引数はデバッグ用の表示に使われ、同じ名前を渡せば同じファイルへつながるという意味ではありません。[1]

シールを使う予定なら、作成時にMFD_ALLOW_SEALINGを指定します。この場合、最初のシール集合は空です。指定しない場合は最初からF_SEAL_SEALがあり、後から別のシールを追加できません。「封印を許可しない」が、内容の書込みまで禁止しているわけではない点に注意が必要です。

固定したい性質を、別々の軸で見る

独自例として、16バイトのファイルに確定した設定値が入っているとします。既存の範囲内へ別の値を書く操作、サイズを8バイトへ縮める操作、32バイトへ広げる操作は、全てデータの見え方を変えますが、同じ制約で止まるとは限りません。

F_SEAL_WRITEを付けると、writeによる内容の変更や、新しい共有・書込み可能なマッピングの作成などが制限されます。一方で、サイズを縮めることや広げることは、このシール単独では禁止されません。縮小を止めるのがF_SEAL_SHRINK、拡大を止めるのがF_SEAL_GROWです。[2]

WRITEのみでは内容変更を禁止しサイズ変更は制限しない。SHRINKとGROWではサイズ変更を禁止し、三つを組み合わせると内容とサイズの両方を制限する。
独自の制約整理。対象操作は既存範囲へのwrite、ftruncateによる縮小・拡大。緑はそのシールが禁止する操作、灰色はそのシールでは禁止しない操作。他の権限や条件による失敗は別に考える。

図で「そのシールでは禁止しない」とした欄は、実機で必ず成功する保証ではありません。呼び出しの引数や権限、他のシール、メモリーやファイルの状態などは別に確認します。目的は、どの性質をどの制約で守るのかを対応させることです。

たとえば、読み手が「16バイトある」と確認した後で、別の参照を持つ処理が8バイトへ縮められるなら、サイズ確認時の前提が利用時まで保たれるとは限りません。内容が書き換わらないことだけを確認しても、範囲が維持されることの確認にはなりません。確定した内容と大きさを渡したい場合には、三つの制約を一緒に検討します。

シールはファイルに付き、追加後には外せない

シールは一つのファイル記述子だけの設定ではなく、inode、すなわちファイルの実体に属する性質です。同じinodeを参照する複数のファイル記述子は、同じシール集合を共有します。自分の記述子を開き直すだけで制約を回避するための仕組みではありません。[2]

F_ADD_SEALSはビットマスクで指定したシールを追加し、成功するとカーネルが直ちに制約を適用します。追加済みのシールを取り除く操作はありません。そのため、作成、サイズの設定、内容の記入、検査、制約の追加という順序を、処理の設計として先に考える必要があります。

F_SEAL_SEALは、この集合への追加そのものを止めます。WRITE、SHRINK、GROWとは役割が異なります。たとえばSHRINKだけを付けた後でSEALを付けると、縮小を禁じた集合が固定されますが、それだけで拡大や内容変更まで禁じた集合に変わるわけではありません。名前のSEALが重なるため、集合の固定とデータの固定を混同しないことが大切です。

既にある書込み経路を残すかどうか

F_SEAL_WRITEを追加しようとしても、そのファイルに共有・書込み可能なマッピングが存在するとEBUSYで失敗します。そのようなマッピングを解除してから追加する必要があります。シールを要求したというログだけでは、内容の固定を確認したことになりません。戻り値と実際のシール集合を確認します。[2]

Linux 5.1で導入されたF_SEAL_FUTURE_WRITEは、既に存在する共有・書込み可能なマッピングを通した内容変更を残せる点が違います。新しい共有・書込み可能なマッピングやwriteによる書込みは制限しますが、設定前から持っていた経路は残ります。

これは、作成側が更新を続け、他の処理には読取り用として共有したい設計で役立ちます。しかし、受け手が内容を確認した直後から永遠に同じ値である、と推論する根拠にはなりません。更新する主体を残すのか、全ての内容変更を止めるのかを、シールの選択と対応させます。本稿の図はF_SEAL_WRITEの比較であり、FUTURE_WRITEへそのまま置き換えた表ではありません。

受け取った側も、実際の制約を読む

F_GET_SEALSは現在のシール集合をビットマスクで返します。受け手は、必要な制約が含まれているかを確認できます。作成側が「確定済み」と説明したことと、必要なシールが付いたことは別の観測です。シールを扱えない対象や操作失敗の場合もあるため、戻り値とerrnoを分けて記録します。

たとえば、内容とサイズの両方が固定されていることを前提にする読み手なら、WRITE、SHRINK、GROWをそれぞれ点検します。さらに制約集合を変えられないことまで必要かは、別の設計判断になります。単一の真偽値として「sealed」と記録するだけでは、この違いが消えてしまいます。

本稿はLinux man-pages 6.19の記述から制約の組合せを整理したもので、Linuxカーネル上で各システムコールを実行した試験ではありません。永続化、暗号化、アクセス権の付与、処理同士の同期までシールだけで保証する説明でもありません。内容、サイズ、変更できる経路、実際の設定結果を分けて追うことが、共有データの前提を正確に保つ出発点です。

出典

  1. Linux man-pages 6.19、memfd_create(2)、DESCRIPTIONとNOTESのFile sealing。
  2. 同、F_GET_SEALS / F_ADD_SEALS、各シールの説明とERRORS。図は指定した操作に対する独自の制約整理。