FlawOpen

flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation

● CVE-2024-0582 · CVSS 7.8 · 高
セキュリティ研究 · FlawOpen

CVE-2024-0582:Linux io_uring のマップ済みバッファリングの解放後使用

CVE-2024-0582(CVSS 7.8、高):IORING_REGISTER_PBUF_RING と IOU_PBUF_RING_MMAP で登録したバッファリングが、ユーザー空間がまだ mmap() でマップしたままの状態で解体時に解放され、カーネルが再利用した後も攻撃者が読み書きできるページが残り、ローカル権限昇格につながりました。

💡 わかりやすい解説 (ELI5)

図書館は専用の閲覧室を登録させ、出入り用の鍵を渡します。あなたが部屋の登録を解除すると、司書はすぐに部屋を貸出可能に戻しますが、鍵は決して回収しません。その後、図書館は同じ部屋を別の人に貸し、あなたは古い鍵で今も入れて、その人が机に残したものを書き換えられます。修正策は、そうした部屋を予約済みのまま保ち、閉館時に、すべての鍵が返却されたあとで一度だけ解放することです。

主要な概念と専門用語

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() でのみ解放します。

ステップ・バイ・ステップの攻撃フロー

ステップ 1

mmap 対応のリングを登録

非特権プロセスが IORING_REGISTER_PBUF_RING と IOU_PBUF_RING_MMAP フラグ付きで io_uring_register() を呼び、カーネルがバッファリングを確保します。

ステップ 2

ユーザー空間にマップ

プロセスがリングを mmap() します。カーネルは remap_pfn_range() でページをマップし、プロセスはマッピングを保持します。

ステップ 3

マップ中にリングを解放

バッファの登録解除(またはリングの解体)が __io_remove_buffers() に到達し、その folio_put() がユーザーマッピングがまだ生きているのにページを解放します。

ステップ 4

古いマッピングから書き込み

カーネルは解放済みページを別のオブジェクトに再割り当てし、マッピングを保持したままのプロセスがそれを読み書きしてカーネル状態を破壊し、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);
	}
}

エンジニアリング&システム堅牢化チェックリスト

参考資料