Repository navigation
Safe Rust APIs can construct invalid GFP flags causing C-side out-of-bounds read #1253
Description
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:
Lines 66 to 71 in 28924df
| 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():
Lines 179 to 196 in 28924df
| /// 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:
- Page allocation path: https://github.com/user-attachments/files/31968031/page_alloc_kasan.log
BitmapVecpath: https://github.com/user-attachments/files/31968032/bitmap_vec_kasan.log
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.
Activity
ojeda commentedon Sep 8, 2026
Yeah, it seems like the C API expects as usual a valid combination of flags, and otherwise one can go out of the
fallbacksarray.I guess, ideally, we would define somehow on the C side what is a valid combination for
alloc_pagesetc. and then check it using that from Rust.Normally, you would try to ping the maintainers of the relevant files by looking at
MAINTAINERS-- let me ping them here:Cc @dakr @Darksonn
I sent an email to the reviewers too.