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)- 커널이 작업마다 하나씩 고르도록 애플리케이션이 등록하는 버퍼 링입니다.
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로 막으세요.