Hello,
While studying the Rust kernel crate, we found a soundness issue where an invalid GFP flag combination can be constructed and passed to the C allocator using only public safe kernel crate functions, resulting in an out-of-bounds read in mm/page_alloc.c.
Confirmed on current upstream master: 28924df2a08f440c73991b83028032c901de2ae4
The attached KASAN logs were collected on v7.1-rc4 (5200f5f49). We also independently reproduced both paths below on the current upstream commit above.
The Problem
At the tested upstream commit, Flags implements the safe ! operator as follows:
|
impl core::ops::Not for Flags { |
|
type Output = Self; |
|
fn not(self) -> Self::Output { |
|
Self(!self.0) |
|
} |
|
} |
This allows safe Rust code to construct GFP flag combinations that are not valid for the C allocator. For example, complementing GFP_ATOMIC sets both __GFP_MOVABLE and __GFP_RECLAIMABLE.
Page::alloc_page() is a public safe Rust function and passes the raw flags directly to alloc_pages():
|
/// Allocate memory for a page and zero its contents. |
|
/// |
|
/// ``` |
|
/// use kernel::page::Page; |
|
/// |
|
/// let page = Page::alloc_page(GFP_KERNEL | __GFP_ZERO)?; |
|
/// # Ok::<(), kernel::alloc::AllocError>(()) |
|
/// ``` |
|
#[inline] |
|
pub fn alloc_page(flags: Flags) -> Result<Self, AllocError> { |
|
// SAFETY: Depending on the value of `gfp_flags`, this call may sleep. Other than that, it |
|
// is always safe to call this method. |
|
let page = unsafe { bindings::alloc_pages(flags.as_raw(), 0) }; |
|
let page = NonNull::new(page).ok_or(AllocError)?; |
|
// INVARIANT: We just successfully allocated a page, so we now have ownership of the newly |
|
// allocated page. We transfer that ownership to the new `Page` object. |
|
Ok(Self { page }) |
|
} |
When both __GFP_MOVABLE and __GFP_RECLAIMABLE are set, gfp_migratetype() returns MIGRATE_HIGHATOMIC (MIGRATE_PCPTYPES). This value is then used to index past the end of the fallbacks table in the page allocator.
KASAN reports:
BUG: KASAN: global-out-of-bounds
Read of size 4
...
The buggy address belongs to the variable:
fallbacks+0x18/0x40
Reproduction
The following reproducer contains two independent triggering paths:
// SPDX-License-Identifier: GPL-2.0
//! Minimal Rust kernel API reproducer.
use kernel::{
alloc::flags::{GFP_ATOMIC, GFP_KERNEL_ACCOUNT},
bitmap::BitmapVec,
page::Page,
prelude::*,
};
module! {
type: RustRepro,
name: "rust_repro",
authors: ["User0"],
description: "Minimal Rust kernel API reproducer",
license: "GPL",
}
struct RustRepro;
fn reproduce_page_alloc() {
pr_info!("rust_repro: reproducing Page::alloc_page path\n");
let flags = core::ops::Not::not(GFP_ATOMIC);
let _page = Page::alloc_page(flags);
pr_info!("rust_repro: Page::alloc_page returned\n");
}
fn reproduce_bitmap_vec() {
pr_info!("rust_repro: reproducing BitmapVec::new path\n");
let flags = core::ops::Not::not(GFP_KERNEL_ACCOUNT);
let _bitmap = BitmapVec::new(0xc0ffee, flags);
pr_info!("rust_repro: BitmapVec::new returned\n");
}
impl kernel::Module for RustRepro {
fn init(_module: &'static ThisModule) -> Result<Self> {
pr_info!("rust_repro: starting reproducer\n");
reproduce_page_alloc();
// reproduce_bitmap_vec();
Ok(Self)
}
}
The Page::alloc_page path is the simpler trigger:
let flags = core::ops::Not::not(GFP_ATOMIC);
let _page = Page::alloc_page(flags);
The second path is:
let flags = core::ops::Not::not(GFP_KERNEL_ACCOUNT);
let _bitmap = BitmapVec::new(0xc0ffee, flags);
For the BitmapVec path, kmalloc_fix_flags() reports and sanitizes the unexpected GFP mask, but both __GFP_MOVABLE and __GFP_RECLAIMABLE remain set, and the same out-of-bounds access is reached.
KASAN logs:
We have not reported this to the mailing list yet because we are unsure which subsystem should own the fix and what the preferred solution would be. We would appreciate the maintainers' guidance on the preferred approach and reporting route.
Hello,
While studying the Rust
kernelcrate, we found a soundness issue where an invalid GFP flag combination can be constructed and passed to the C allocator using only public safekernelcrate functions, resulting in an out-of-bounds read inmm/page_alloc.c.Confirmed on current upstream master:
28924df2a08f440c73991b83028032c901de2ae4The attached KASAN logs were collected on
v7.1-rc4(5200f5f49). We also independently reproduced both paths below on the current upstream commit above.The Problem
At the tested upstream commit,
Flagsimplements the safe!operator as follows:linux/rust/kernel/alloc.rs
Lines 66 to 71 in 28924df
This allows safe Rust code to construct GFP flag combinations that are not valid for the C allocator. For example, complementing
GFP_ATOMICsets both__GFP_MOVABLEand__GFP_RECLAIMABLE.Page::alloc_page()is a public safe Rust function and passes the raw flags directly toalloc_pages():linux/rust/kernel/page.rs
Lines 179 to 196 in 28924df
When both
__GFP_MOVABLEand__GFP_RECLAIMABLEare set,gfp_migratetype()returnsMIGRATE_HIGHATOMIC(MIGRATE_PCPTYPES). This value is then used to index past the end of thefallbackstable in the page allocator.KASAN reports:
Reproduction
The following reproducer contains two independent triggering paths:
The
Page::alloc_pagepath is the simpler trigger:The second path is:
For the
BitmapVecpath,kmalloc_fix_flags()reports and sanitizes the unexpected GFP mask, but both__GFP_MOVABLEand__GFP_RECLAIMABLEremain set, and the same out-of-bounds access is reached.KASAN logs:
BitmapVecpath: https://github.com/user-attachments/files/31968032/bitmap_vec_kasan.logWe have not reported this to the mailing list yet because we are unsure which subsystem should own the fix and what the preferred solution would be. We would appreciate the maintainers' guidance on the preferred approach and reporting route.