flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation
CVE-2024-0582:Linux io_uring のマップ済みバッファリングの解放後使用
CVE-2024-0582(CVSS 7.8、高):IORING_REGISTER_PBUF_RING と IOU_PBUF_RING_MMAP で登録したバッファリングが、ユーザー空間がまだ mmap() でマップしたままの状態で解体時に解放され、カーネルが再利用した後も攻撃者が読み書きできるページが残り、ローカル権限昇格につながりました。
図書館は専用の閲覧室を登録させ、出入り用の鍵を渡します。あなたが部屋の登録を解除すると、司書はすぐに部屋を貸出可能に戻しますが、鍵は決して回収しません。その後、図書館は同じ部屋を別の人に貸し、あなたは古い鍵で今も入れて、その人が机に残したものを書き換えられます。修正策は、そうした部屋を予約済みのまま保ち、閉館時に、すべての鍵が返却されたあとで一度だけ解放することです。
主要な概念と専門用語
io_uring- Linux の非同期 I/O インターフェース。アプリとカーネルが送信リングと完了リングを共有し、操作ごとのシステムコールを避けます。
提供バッファリング(IORING_REGISTER_PBUF_RING)- アプリが登録し、カーネルが操作ごとに1つ選ぶバッファのリング。
IOU_PBUF_RING_MMAPを付けると、カーネルがリングを確保し、アプリは自前のメモリを渡す代わりにmmap()でマップします。 解放後使用(UAF)- 解放された後のメモリを使うこと。カーネルがそのメモリを別のオブジェクトに再割り当てすると、古い参照からそれを読み書き・破壊できます。
remap_pfn_range / folio_put- リングは
remap_pfn_range()でユーザー空間にマップされるため、通常の munmap の会計では解放されません。解体時にfolio_put()を呼ぶと、マッピングがまだ生きているのにページを解放していました。
フレームワーク対応
- このガイドで説明する弱点の CWE 参照: CWE-416
根本原因の分析 (Root Cause)
IOU_PBUF_RING_MMAP で作られたバッファリングでは、io_alloc_pbuf_ring() がページを確保し、ユーザー空間が remap_pfn_range() 経由の mmap() でそれらをマップします。この経路は通常の munmap/release の会計を通らないため、__io_remove_buffers() の folio_put() は、ユーザーマッピングがまだ存在しうる状態でページを解放していました。これは解放後使用(CWE-416)で、ユーザー空間はカーネルがアロケータに返却して再割り当てできるページへの有効なマッピングを保持し続けます。Jann Horn が報告しました。コミット c392cbecd8ec は mmap で確保した各リングを新しい io_buf_list に記録し、マッピングが消えたあとの ->release() の io_kbuf_mmap_list_free() でのみ解放します。
ステップ・バイ・ステップの攻撃フロー
mmap 対応のリングを登録
非特権プロセスが IORING_REGISTER_PBUF_RING と IOU_PBUF_RING_MMAP フラグ付きで io_uring_register() を呼び、カーネルがバッファリングを確保します。
ユーザー空間にマップ
プロセスがリングを mmap() します。カーネルは remap_pfn_range() でページをマップし、プロセスはマッピングを保持します。
マップ中にリングを解放
バッファの登録解除(またはリングの解体)が __io_remove_buffers() に到達し、その folio_put() がユーザーマッピングがまだ生きているのにページを解放します。
古いマッピングから書き込み
カーネルは解放済みページを別のオブジェクトに再割り当てし、マッピングを保持したままのプロセスがそれを読み書きしてカーネル状態を破壊し、root に昇格します。
ソースコード比較:脆弱 vs 堅牢化
/* io_uring/kbuf.c の修正前 (Linux 6.6.4) */
static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
struct io_buffer_list *bl)
{
gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
size_t ring_size;
void *ptr;
ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
if (!ptr)
return -ENOMEM;
bl->buf_ring = ptr;
bl->is_mapped = 1;
bl->is_mmap = 1;
return 0;
}
static int __io_remove_buffers(struct io_ring_ctx *ctx,
struct io_buffer_list *bl, unsigned nbufs)
{
if (bl->is_mapped) {
i = bl->buf_ring->tail - bl->head;
if (bl->is_mmap) {
/* 不具合: ring は mmap() と
* remap_pfn_range() によってユーザー空間へ渡されていた。ここで folio_put() がページを解放すると、
* マッピングがまだ存在する可能性があり、ユーザー空間は解放後に再割り当てされた
* カーネルページへ書き込み続けてしまう。 */
folio_put(virt_to_folio(bl->buf_ring));
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* io_uring/kbuf.c、commit c392cbecd8ec 適用後 (Linux 6.6.5 / 6.7) */
struct io_buf_free {
struct hlist_node list;
void *mem;
};
static int io_alloc_pbuf_ring(struct io_ring_ctx *ctx,
struct io_uring_buf_reg *reg,
struct io_buffer_list *bl)
{
struct io_buf_free *ibf;
size_t ring_size;
void *ptr;
ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
ptr = io_mem_alloc(ring_size);
if (!ptr)
return -ENOMEM;
/* 修正: mmap 可能な ring をすべて記録し、そのページは
* マッピングが消えた後の ->release() 時にのみ解放する。teardown 中には解放しない。 */
ibf = kmalloc(sizeof(*ibf), GFP_KERNEL_ACCOUNT);
if (!ibf) {
io_mem_free(ptr);
return -ENOMEM;
}
ibf->mem = ptr;
hlist_add_head(&ibf->list, &ctx->io_buf_list);
bl->buf_ring = ptr;
bl->is_mapped = 1;
bl->is_mmap = 1;
return 0;
}
static int __io_remove_buffers(struct io_ring_ctx *ctx,
struct io_buffer_list *bl, unsigned nbufs)
{
if (bl->is_mapped) {
i = bl->buf_ring->tail - bl->head;
if (bl->is_mmap) {
/* io_kbuf_mmap_list_free() は ->release() 時にページを解放するため、
* マッピングされている可能性がある間はここで解放しない。 */
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* ->release() 時またはその後に呼び出す。これで mmap 済みの ring を安全に解放できる。 */
void io_kbuf_mmap_list_free(struct io_ring_ctx *ctx)
{
struct io_buf_free *ibf;
struct hlist_node *tmp;
hlist_for_each_entry_safe(ibf, tmp, &ctx->io_buf_list, list) {
hlist_del(&ibf->list);
io_mem_free(ibf->mem);
kfree(ibf);
}
}
エンジニアリング&システム堅牢化チェックリスト
- ✓コミット c392cbecd8ec を含むカーネル(Linux 6.6.5、6.7 以降、または変更履歴に
CVE-2024-0582が載っているディストリビューションのカーネル)を使う。 - ✓ワークロードが io_uring を必要としないなら制限する:
kernel.io_uring_disabled=2を設定するか、ディストリビューションの seccomp/SELinux でワークロードごとに制御する。 - ✓
mmap()/remap_pfn_range()でユーザー空間にメモリを渡すときは、その寿命を早い解体経路ではなく->release()に結び付け、マッピングが消えたあとに一度だけ解放する。 - ✓KASAN と
slub_debugを有効にしてカーネルをビルド・ファジングし、マップ中の解放を解放後使用としてテストで捕まえる。 - ✓信頼できないローカルワークロードには、
CONFIG_IO_URINGなしでビルドしたカーネルを選ぶか、io_uring_setup/io_uring_registerシステムコールを seccomp でブロックする。