| // Copyright 2026 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| use crate::counters::define_kcounter; |
| use crate::kernel::deadline::{Deadline, SlackMode, TimerSlack}; |
| use crate::kernel::event::{AutounsignalEvent, Event}; |
| use crate::kernel::thread::{AutoPreemptDisabler, THREAD_SIGNAL_SUSPEND}; |
| use crate::kernel::types::PAddr; |
| use crate::vm::compression::VmCompression; |
| use crate::vm::evictor::Evictor; |
| use crate::vm::page::{VmPage, VmPageDoublyLinkedList, VmPagePtr}; |
| use crate::vm::page_queues::PageQueues; |
| use crate::vm::page_state::VmPageState; |
| use crate::vm::physmap::paddr_to_physmap; |
| use crate::vm::pmm_arena::{PmmArena, PmmArenaInfo, PmmStateCount, print_page_state_counts}; |
| use crate::vm::pmm_checker::{CheckFailAction, PmmChecker}; |
| use boot_options::BootOptions; |
| use core::cell::UnsafeCell; |
| use core::mem::MaybeUninit; |
| use core::pin::Pin; |
| use core::ptr::NonNull; |
| use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; |
| use debug::{dprintf, ltracef}; |
| use fbl::{DoublyLinkedList, SinglyLinkedListNode}; |
| use ksync::{KMutex, LockToken, RawMutex, guarded, kcell_init}; |
| use pin_init::{PinInit, Wrapper, pin_data, pin_init}; |
| use pmm_node_bindings as bindings; |
| use zx_status::Status; |
| use zx_types::{zx_duration_t, zx_instant_mono_t, zx_status_t}; |
| |
| const LOCAL_TRACE: u32 = 0; |
| |
| // TODO(https://fxbug.dev/549318457): Ideally we would use an rng from the standard rand crate, but |
| // even the smallest rng source is 128 bits, and to maintain data layout compatibility with C++ it |
| // must be 64-bits. Once C++ no longer references the data structure directly this can be dropped, |
| // the C library rand_r no longer used and this definition removed. |
| const RAND_MAX: core::ffi::c_int = 0x7fff_ffff; |
| |
| // TODO(https://fxbug.dev/521834554): Replicating the C++ version of this constant here until a |
| // Rust instrumentation is supported. |
| const ASAN_PMM_FREE_MAGIC: u8 = 0xfb; |
| |
| define_kcounter!(PMM_ALLOC_FAILED, "vm.pmm.alloc.failed", Sum); |
| define_kcounter!(PMM_ALLOC_DELAYED, "vm.pmm.alloc.delayed", Sum); |
| |
| pub type AllocFailureType = bindings::PmmNode_AllocFailure_Type; |
| |
| /// This enum is used to specify whether a page, when freed, should have its reuse |
| /// (i.e. reallocation) delayed. This feature exists to both improve the PMM checker's ability |
| /// to detect "bad DMAs" and to reduce the impact when they do occur. When allocating a page, |
| /// reusing the most recently freed page often has performance benefits. However, it can |
| /// amplify the impact of use-after-free bugs. This feature enables part of the kernel to |
| /// express a preference on whether a page should be eligible for immediate reuse or not. It's |
| /// a hint. |
| /// |
| /// |Default| means no preference. When specified, the page may or may not be immediately reused. |
| /// In some build/runtime configurations (e.g. kasan) delayed reuse is the default behavior. |
| /// |
| /// |Yes| indicates that the PMM should delay the reuse of the page by placing it on the "cold" end |
| /// up the free list, thereby maximizing the amount of time before which it is reallocated. |
| pub use bindings::PmmOptDelayReuse; |
| |
| // Flags for PMM allocation routines. |
| /// no restrictions on which arena to allocate from. |
| pub const ALLOC_FLAG_ANY: u32 = bindings::PMM_ALLOC_FLAG_ANY; |
| /// The caller is able to wait and retry this allocation and so pmm allocation functions are allowed |
| /// to return ZX_ERR_SHOULD_WAIT, as opposed to ZX_ERR_NO_MEMORY, to indicate that the caller should |
| /// wait and try again. This is intended for the PMM to tell callers who are able to wait that |
| /// memory is low. The caller should not infer anything about memory state if it is told to wait, as |
| /// the PMM may tell it to wait for any reason. |
| pub const ALLOC_FLAG_CAN_WAIT: u32 = bindings::PMM_ALLOC_FLAG_CAN_WAIT; |
| |
| /// Tell this PmmNode that we've failed a user-visible allocation. Calling this method will |
| /// (optionally) trigger an asynchronous OOM response. To improve diagnostics some information |
| /// about the source of the failure can be provided. |
| #[repr(C)] |
| #[derive(Debug, Copy, Clone)] |
| pub struct AllocFailure { |
| pub r#type: AllocFailureType, |
| pub size: usize, |
| pub free_count: u64, |
| } |
| |
| impl Default for AllocFailure { |
| fn default() -> Self { |
| Self { r#type: AllocFailureType::None, size: 0, free_count: 0 } |
| } |
| } |
| |
| // Compile-time layout assertions against C++ AllocFailure |
| zr::static_assert!( |
| core::mem::size_of::<AllocFailure>() == core::mem::size_of::<bindings::PmmNode_AllocFailure>() |
| ); |
| zr::static_assert!( |
| core::mem::align_of::<AllocFailure>() |
| == core::mem::align_of::<bindings::PmmNode_AllocFailure>() |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(AllocFailure, r#type) |
| == core::mem::offset_of!(bindings::PmmNode_AllocFailure, type_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(AllocFailure, size) |
| == core::mem::offset_of!(bindings::PmmNode_AllocFailure, size) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(AllocFailure, free_count) |
| == core::mem::offset_of!(bindings::PmmNode_AllocFailure, free_count) |
| ); |
| |
| /// Controls the behavior of requests that have the PMM_ALLOC_FLAG_CAN_WAIT. |
| #[repr(u32)] |
| #[derive(Debug, Clone, Copy, Eq, PartialEq)] |
| pub enum ShouldWaitState { |
| /// The PMM_ALLOC_FLAG_CAN_WAIT should never be followed and we will always attempt to perform |
| /// the allocation, or fail with ZX_ERR_NO_MEMORY. This state is permanent and cannot be left. |
| Never, |
| /// Allocations do not need to be delayed, but the should_wait_free_pages_level should be |
| /// monitored and once tripped should be delayed. |
| OnceLevelTripped, |
| /// State indicates that the level got tripped, and we should delay any allocations until the |
| /// level is reset. |
| UntilReset, |
| } |
| |
| // Compile-time layout assertions against C++ ShouldWaitState |
| zr::static_assert!( |
| core::mem::size_of::<ShouldWaitState>() |
| == core::mem::size_of::<bindings::PmmNode_ShouldWaitState>() |
| ); |
| zr::static_assert!( |
| ShouldWaitState::Never as u32 == bindings::PmmNode_ShouldWaitState::Never as u32 |
| ); |
| zr::static_assert!( |
| ShouldWaitState::OnceLevelTripped as u32 |
| == bindings::PmmNode_ShouldWaitState::OnceLevelTripped as u32 |
| ); |
| zr::static_assert!( |
| ShouldWaitState::UntilReset as u32 == bindings::PmmNode_ShouldWaitState::UntilReset as u32 |
| ); |
| |
| /// Waiter node for loaned page freeing synchronization. |
| #[derive(fbl::SinglyLinkedListContainable)] |
| #[pin_data] |
| #[repr(C)] |
| pub struct FreeLoanedPagesHolderWaiter { |
| #[sll_node] |
| pub node: fbl::SinglyLinkedListNode<FreeLoanedPagesHolderWaiter>, |
| #[pin] |
| pub event: Event, |
| } |
| |
| // Compile-time layout assertions against C++ Waiter |
| zr::static_assert!( |
| core::mem::size_of::<FreeLoanedPagesHolderWaiter>() |
| == core::mem::size_of::<bindings::FreeLoanedPagesHolder_Waiter>() |
| ); |
| zr::static_assert!( |
| core::mem::align_of::<FreeLoanedPagesHolderWaiter>() |
| == core::mem::align_of::<bindings::FreeLoanedPagesHolder_Waiter>() |
| ); |
| |
| /// Object for managing freeing of loaned pages via a temporary holding object. Can be instantiated |
| /// on the stack and then passed into different PmmNode methods, the object itself has no publicly |
| /// available methods. |
| /// This object is not thread safe, and multiple threads must not pass the same instance of this |
| /// object into PmmNode methods. |
| /// A given FreeLoanedPagesHolder, as described in |FinishFreeLoanedPages|, may only be used for a |
| /// single call to |FinishFreeLoanedPages|, after which it is 'dead', and may not be passed to any |
| /// other PmmNode methods. |
| #[pin_data(PinnedDrop)] |
| #[repr(C)] |
| pub struct FreeLoanedPagesHolder { |
| /// A given FreeLoanedPagesHolder interval is only allowed to be used once to return pages to |
| /// the PMM, this tracks whether this has happened or not. |
| /// Only permitting a single instance of freeing simplifies any need to reason about a single |
| /// FreeLoanedPagesHolder repeatedly having pages moved into it and free'd to the PMM |
| /// concurrently with attempts to wait on it. |
| /// Although the lock cannot be annotated, this member is guarded by the relevant |
| /// PmmNode::loaned_list_lock_. |
| pub used: bool, |
| /// List of pages presently owned by this object. Every page in this list is defined to be in |
| /// the ALLOC state with |owner| set to this object. |
| /// Although the lock cannot be annotated, this member is guarded by the relevant |
| /// PmmNode::loaned_list_lock_. |
| #[pin] |
| pub pages: VmPageDoublyLinkedList, |
| /// Maintain a list of waiters to be notified once pages have been freed. The Waiter object |
| /// itself is stack allocated in the WithLoanedPage method and registered into this list. |
| /// Having this be a list of Events of single waiting thread, instead of a single Event |
| /// with a list of waiting threads, allows the waiters to retain a reference to the FLPH |
| /// object while waiting. This ensures that once FinishFreeLoanedPages performs the signal |
| /// on the waiters, the FLPH object can be safely destroyed. |
| pub waiters: fbl::SinglyLinkedList<*mut FreeLoanedPagesHolderWaiter>, |
| } |
| |
| // Compile-time layout assertions against C++ FreeLoanedPagesHolder |
| zr::static_assert!( |
| core::mem::size_of::<FreeLoanedPagesHolder>() |
| == core::mem::size_of::<bindings::FreeLoanedPagesHolder>() |
| ); |
| zr::static_assert!( |
| core::mem::align_of::<FreeLoanedPagesHolder>() |
| == core::mem::align_of::<bindings::FreeLoanedPagesHolder>() |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(FreeLoanedPagesHolder, used) |
| == core::mem::offset_of!(bindings::FreeLoanedPagesHolder, used_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(FreeLoanedPagesHolder, pages) |
| == core::mem::offset_of!(bindings::FreeLoanedPagesHolder, pages_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(FreeLoanedPagesHolder, waiters) |
| == core::mem::offset_of!(bindings::FreeLoanedPagesHolder, waiters_) |
| ); |
| |
| impl FreeLoanedPagesHolder { |
| /// Creates a new pinned initializer for `FreeLoanedPagesHolder`. |
| pub fn init() -> impl pin_init::PinInit<Self, core::convert::Infallible> { |
| pin_init::pin_init!(&_this in Self { |
| used: false, |
| pages <- fbl::DoublyLinkedList::new(), |
| waiters: fbl::SinglyLinkedList::new(), |
| }) |
| } |
| } |
| |
| #[pin_init::pinned_drop] |
| impl PinnedDrop for FreeLoanedPagesHolder { |
| fn drop(self: core::pin::Pin<&mut Self>) { |
| assert!(self.pages.is_empty()); |
| assert!(self.waiters.is_empty()); |
| } |
| } |
| |
| unsafe extern "C" { |
| fn asan_poison_shadow(address: usize, size: usize, value: u8); |
| fn asan_unpoison_shadow(address: usize, size: usize); |
| fn rand_r(seed: *mut usize) -> core::ffi::c_int; |
| fn cpp_global_prng_draw(buffer: *mut u8, len: usize); |
| } |
| |
| fn asan_poison_page(page: &VmPage, value: u8) { |
| if cfg!(sanitize = "address") { |
| // SAFETY: FFI call to C++ asan shim. |
| unsafe { |
| asan_poison_shadow(paddr_to_physmap(page.paddr()).0, page::SIZE, value); |
| } |
| } |
| } |
| |
| fn asan_unpoison_page(page: &VmPage) { |
| if cfg!(sanitize = "address") { |
| // SAFETY: FFI call to C++ asan shim. |
| unsafe { |
| asan_unpoison_shadow(paddr_to_physmap(page.paddr()).0, page::SIZE); |
| } |
| } |
| } |
| |
| fn return_pages_to_free_list( |
| target_list: &mut VmPageDoublyLinkedList, |
| to_free: &mut VmPageDoublyLinkedList, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| if delay_reuse == PmmOptDelayReuse::Yes || cfg!(sanitize = "address") { |
| target_list.splice(to_free); |
| } else { |
| target_list.cursor_front_mut().splice(to_free); |
| } |
| } |
| |
| /// Per-NUMA node collection of physical memory arenas and bookkeeping. |
| #[repr(C)] |
| #[guarded] |
| pub struct PmmNode { |
| canary: fbl::Canary<{ fbl::magic(b"PNOD") }>, |
| |
| #[mutex] |
| lock: KMutex<RawMutex>, |
| |
| #[guarded_by(lock)] |
| arena_cumulative_size: u64, |
| // This is both an atomic and guarded by lock as we would like modifications to require the |
| // lock, as logic in the system relies on the free_count not changing whilst the lock is |
| // held, but also be an atomic so it can be correctly read without the lock. |
| #[guarded_by(lock)] |
| free_count: AtomicU64, |
| #[guarded_by(loaned_list_lock)] |
| free_loaned_count: AtomicU64, |
| #[guarded_by(loaned_list_lock)] |
| loaned_count: AtomicU64, |
| #[guarded_by(loaned_list_lock)] |
| loan_cancelled_count: AtomicU64, |
| |
| /// Free pages where !loaned. |
| #[guarded_by(lock)] |
| #[pin] |
| free_list: VmPageDoublyLinkedList, |
| #[mutex] |
| loaned_list_lock: KMutex<RawMutex>, |
| /// Free pages where loaned && !loan_cancelled. |
| #[guarded_by(loaned_list_lock)] |
| #[pin] |
| free_loaned_list: VmPageDoublyLinkedList, |
| |
| /// The pages comprising the memory temporarily used during phys hand-off, |
| /// populated on Init(). It is the responsibility of EndHandoff() to free this |
| /// list. |
| #[pin] |
| phys_handoff_temporary_list: UnsafeCell<VmPageDoublyLinkedList>, |
| |
| /// The pages comprising the page-aligned regions of memory that we expect to |
| /// turn into VMOs to hand-off to userspace - as determined by |
| /// PhysHandoff::IsPhysVmoType() - populated and marked as wired on Init(). |
| /// |
| /// It is expected that this memory will be unwired and turned into VMOs by the |
| /// end of the phys hand-off phase, and it is the responsibility of |
| /// PmmNode::EndHandoff() to ensure afterward that this list is empty. |
| #[guarded_by(lock)] |
| #[pin] |
| phys_handoff_vmo_list: VmPageDoublyLinkedList, |
| |
| /// The pages intended to be permanently reserved. |
| #[guarded_by(lock)] |
| #[pin] |
| permanently_reserved_list: VmPageDoublyLinkedList, |
| |
| #[guarded_by(lock)] |
| should_wait: ShouldWaitState, |
| |
| /// Below this number of free pages the PMM will transition into delaying allocations. |
| #[guarded_by(lock)] |
| should_wait_free_pages_level: u64, |
| |
| /// The event acts a gate keeper for waking up threads waiting for allocations one at time. |
| /// The event gets signalled when there MAY be pages available. |
| #[pin] |
| may_allocate_evt: AutounsignalEvent, |
| |
| /// Indicates whether a PMM alloc call has ever failed with ZX_ERR_NO_MEMORY. Used to trigger |
| /// an OOM response. See |MemoryWatchdog::WorkerThread|. |
| alloc_failed_no_mem: AtomicBool, |
| |
| /// A record of the first time an allocation failure is reported to aid in diagnostics. |
| #[guarded_by(lock)] |
| first_alloc_failure: AllocFailure, |
| |
| /// If mem_signal is not null, then once the available free memory falls outside of the |
| /// defined lower and upper bound the signal is raised. This is a one-shot signal and is |
| /// cleared after firing. |
| #[guarded_by(lock)] |
| mem_signal: *mut Event, |
| #[guarded_by(lock)] |
| mem_signal_lower_bound: u64, |
| #[guarded_by(lock)] |
| mem_signal_upper_bound: u64, |
| |
| #[pin] |
| page_queues: UnsafeCell<PageQueues>, |
| |
| #[pin] |
| evictor: UnsafeCell<Evictor>, |
| |
| #[mutex] |
| compression_lock: KMutex<RawMutex>, |
| /// The page_compression is a lazily initialized RefPtr to keep the PmmNode constructor |
| /// simple, at the cost needing to hold a lock to read the RefPtr. To avoid unnecessarily |
| /// contending on the main pmm lock, use a separate one. |
| #[guarded_by(compression_lock)] |
| page_compression: Option<fbl::RefPtr<VmCompression>>, |
| |
| /// Indicates whether pages should have a pattern filled into them when they are freed. This |
| /// value can only transition from false->true, and never back to false again. Once this |
| /// value is set, the fill size in checker may no longer be changed, and it becomes safe |
| /// to call FillPattern even without the lock held. |
| /// This is an atomic to allow for reading this outside of the lock, but modifications only |
| /// happen with the lock held. |
| #[guarded_by(lock, loaned_list_lock)] |
| free_fill_enabled: AtomicBool, |
| /// Indicates whether it is known that all pages in the free list have had a pattern filled |
| /// into them. This value can only transition from false->true, and never back to false |
| /// again. Once this value is set the action and armed state in checker may no longer be |
| /// changed, and it becomes safe to call AssertPattern even without the lock held. |
| #[guarded_by(lock, loaned_list_lock)] |
| // TODO(https://fxbug.dev/562635905): guarded_by currently does not support multiple locks and |
| // so fails to event generate the wrapping UnsafeCell around this method. For now manually put |
| // the UnsafeCell in and require callers to manually manage the locking. |
| all_free_pages_filled: UnsafeCell<bool>, |
| #[pin] |
| checker: UnsafeCell<PmmChecker>, |
| |
| /// The rng state for random waiting on allocations. This allows us to use rand_r, which |
| /// requires no further thread synchronization, unlike rand(). |
| #[guarded_by(lock)] |
| random_should_wait_seed: usize, |
| |
| #[guarded_by(lock)] |
| used_arena_count: usize, |
| #[guarded_by(lock)] |
| arenas: [PmmArena; PmmNode::ARENA_COUNT], |
| |
| phantom: core::marker::PhantomData<core::marker::PhantomPinned>, |
| } |
| |
| // Compile-time layout assertions against the C++ PmmNode type via bindgen. |
| zr::static_assert!(core::mem::size_of::<PmmNode>() == core::mem::size_of::<bindings::PmmNode>()); |
| zr::static_assert!(core::mem::align_of::<PmmNode>() == core::mem::align_of::<bindings::PmmNode>()); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, canary) == core::mem::offset_of!(bindings::PmmNode, canary_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, lock) == core::mem::offset_of!(bindings::PmmNode, lock_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, arena_cumulative_size) |
| == core::mem::offset_of!(bindings::PmmNode, arena_cumulative_size_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, free_count) |
| == core::mem::offset_of!(bindings::PmmNode, free_count_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, free_loaned_count) |
| == core::mem::offset_of!(bindings::PmmNode, free_loaned_count_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, loaned_count) |
| == core::mem::offset_of!(bindings::PmmNode, loaned_count_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, loan_cancelled_count) |
| == core::mem::offset_of!(bindings::PmmNode, loan_cancelled_count_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, free_list) |
| == core::mem::offset_of!(bindings::PmmNode, free_list_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, loaned_list_lock) |
| == core::mem::offset_of!(bindings::PmmNode, loaned_list_lock_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, free_loaned_list) |
| == core::mem::offset_of!(bindings::PmmNode, free_loaned_list_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, phys_handoff_temporary_list) |
| == core::mem::offset_of!(bindings::PmmNode, phys_handoff_temporary_list_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, phys_handoff_vmo_list) |
| == core::mem::offset_of!(bindings::PmmNode, phys_handoff_vmo_list_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, permanently_reserved_list) |
| == core::mem::offset_of!(bindings::PmmNode, permanently_reserved_list_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, should_wait) |
| == core::mem::offset_of!(bindings::PmmNode, should_wait_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, should_wait_free_pages_level) |
| == core::mem::offset_of!(bindings::PmmNode, should_wait_free_pages_level_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, may_allocate_evt) |
| == core::mem::offset_of!(bindings::PmmNode, may_allocate_evt_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, alloc_failed_no_mem) |
| == core::mem::offset_of!(bindings::PmmNode, alloc_failed_no_mem_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, first_alloc_failure) |
| == core::mem::offset_of!(bindings::PmmNode, first_alloc_failure_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, mem_signal) |
| == core::mem::offset_of!(bindings::PmmNode, mem_signal_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, mem_signal_lower_bound) |
| == core::mem::offset_of!(bindings::PmmNode, mem_signal_lower_bound_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, mem_signal_upper_bound) |
| == core::mem::offset_of!(bindings::PmmNode, mem_signal_upper_bound_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, page_queues) |
| == core::mem::offset_of!(bindings::PmmNode, page_queues_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, evictor) == core::mem::offset_of!(bindings::PmmNode, evictor_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, compression_lock) |
| == core::mem::offset_of!(bindings::PmmNode, compression_lock_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, page_compression) |
| == core::mem::offset_of!(bindings::PmmNode, page_compression_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, free_fill_enabled) |
| == core::mem::offset_of!(bindings::PmmNode, free_fill_enabled_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, all_free_pages_filled) |
| == core::mem::offset_of!(bindings::PmmNode, all_free_pages_filled_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, checker) == core::mem::offset_of!(bindings::PmmNode, checker_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, random_should_wait_seed) |
| == core::mem::offset_of!(bindings::PmmNode, random_should_wait_seed_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, used_arena_count) |
| == core::mem::offset_of!(bindings::PmmNode, used_arena_count_) |
| ); |
| zr::static_assert!( |
| core::mem::offset_of!(PmmNode, arenas) == core::mem::offset_of!(bindings::PmmNode, arenas_) |
| ); |
| |
| unsafe impl Sync for PmmNode {} |
| unsafe impl Send for PmmNode {} |
| |
| impl PmmNode { |
| /// Arenas are allocated from the node itself to avoid any boot allocations. Walking linearly |
| /// through them at run time should also be fairly efficient. |
| const ARENA_COUNT: usize = bindings::PmmNode_kArenaCount; |
| /// Bit constants to fold a vm_page_t pointer into a uint32 and back. The format from LSB |
| /// to MSB is : zero-bits | arena-index | page-index |. Where arena-index is 4 bits wide and |
| /// zero-bits is 3 bits wide. This limits the number of pages per arena to 2^25. |
| const ARENA_BITS: i32 = bindings::PmmNode_kArenaBits; |
| pub const INDEX_ZERO_BITS: i32 = bindings::PmmNode_kIndexZeroBits; |
| const MAX_PAGES_PER_ARENA: usize = bindings::PmmNode_kMaxPagesPerArena; |
| const ARENA_MASK: u32 = bindings::PmmNode_kArenaMask; |
| |
| /// Creates an in-place pinned initializer for `PmmNode`. |
| pub fn init() -> impl PinInit<Self, core::convert::Infallible> { |
| pin_init!(Self { |
| canary: fbl::Canary::new(), |
| lock <- KMutex::init(), |
| arena_cumulative_size: 0.into(), |
| free_count: AtomicU64::new(0).into(), |
| free_loaned_count: AtomicU64::new(0).into(), |
| loaned_count: AtomicU64::new(0).into(), |
| loan_cancelled_count: AtomicU64::new(0).into(), |
| free_list <- kcell_init(fbl::DoublyLinkedList::new()), |
| loaned_list_lock <- KMutex::init(), |
| free_loaned_list <- kcell_init(fbl::DoublyLinkedList::new()), |
| phys_handoff_temporary_list <- UnsafeCell::pin_init(fbl::DoublyLinkedList::new()), |
| phys_handoff_vmo_list <- kcell_init(fbl::DoublyLinkedList::new()), |
| permanently_reserved_list <- kcell_init(fbl::DoublyLinkedList::new()), |
| should_wait: ShouldWaitState::OnceLevelTripped.into(), |
| should_wait_free_pages_level: 0.into(), |
| may_allocate_evt <- AutounsignalEvent::init_signaled(), |
| alloc_failed_no_mem: AtomicBool::new(false), |
| first_alloc_failure: AllocFailure::default().into(), |
| mem_signal: core::ptr::null_mut::<Event>().into(), |
| mem_signal_lower_bound: 0.into(), |
| mem_signal_upper_bound: 0.into(), |
| page_queues <- UnsafeCell::pin_init(PageQueues::init()), |
| evictor <- UnsafeCell::pin_init(Evictor::init()), |
| compression_lock <- KMutex::init(), |
| page_compression: None.into(), |
| free_fill_enabled: AtomicBool::new(false), |
| all_free_pages_filled: UnsafeCell::new(false), |
| checker <- UnsafeCell::pin_init(PmmChecker::init()), |
| random_should_wait_seed: 0.into(), |
| used_arena_count: 0.into(), |
| arenas: [const { PmmArena::new() }; PmmNode::ARENA_COUNT].into(), |
| phantom: core::marker::PhantomData, |
| }) |
| } |
| |
| /// Domain-specific conversion: returns raw pointer for `PmmNode`. |
| pub fn as_raw(&self) -> *mut bindings::PmmNode { |
| (self as *const Self).cast_mut().cast() |
| } |
| |
| /// Return the slice of arenas from the built-in array that are known to be active. Used in |
| /// loops that iterate across all arenas. |
| fn active_arenas<'a>(&'a self, token: &'a LockToken<'_, PmmNodeLockClass>) -> &'a [PmmArena] { |
| // SAFETY: The lock token proves that either the lock protecting these fields is held, or |
| // the caller has determined it is safe. |
| unsafe { &self.arenas.get(token)[..*self.used_arena_count.get(token)] } |
| } |
| |
| /// Converts the number returned by page_to_index() back to a VmPagePtr pointer. |
| /// It does not check for invalid indexes such as 0. |
| /// |
| /// Note: This method is faster than page_to_index, about the cost of some basic math |
| /// and bit manipulation. |
| /// |
| /// # Safety |
| /// |
| /// The `index` must be a valid PMM page index. |
| pub unsafe fn index_to_page(&self, index: u32) -> VmPagePtr { |
| let index = index >> Self::INDEX_ZERO_BITS; |
| let arena_ix = (index & Self::ARENA_MASK) as usize; |
| let page_ix = (index >> Self::ARENA_BITS) as usize; |
| // SAFETY: Active arenas are initialized during early boot and arena metadata is immutable. |
| unsafe { |
| let token = LockToken::new(); |
| VmPagePtr::new(self.active_arenas(&token)[arena_ix].get_page(page_ix - 1)) |
| } |
| } |
| |
| /// Returns compressed representation a page_t*, with the following characteristics: |
| /// - zeros in the last INDEZ_ZERO_BITS bits, used by clients to store metadata. |
| /// - The value 0 is never returned, it can be used as "no page" marker. |
| /// |
| /// Note: This method needs to traverse (up to) all the memory pools so it's cost is |
| /// low but not trivial. |
| pub fn page_to_index(&self, page: VmPagePtr) -> u32 { |
| let page_raw = page.as_raw(); |
| // SAFETY: Active arenas are initialized during early boot and arena metadata is immutable. |
| let token = unsafe { LockToken::new() }; |
| for (arena_ix, a) in self.active_arenas(&token).iter().enumerate() { |
| // SAFETY: `page` is a valid VmPagePtr so `page_raw` is a valid pointer. |
| if unsafe { a.page_belongs_to_arena(page_raw) } { |
| // SAFETY: `page_raw` belongs to this arena's `page_array`. |
| let page_ix = (unsafe { a.get_index(page_raw) } + 1) as u32; |
| return ((page_ix << Self::ARENA_BITS) | (arena_ix as u32)) |
| << Self::INDEX_ZERO_BITS; |
| } |
| } |
| 0 |
| } |
| |
| /// Converts the number returned by page_to_index() back to a PAddr. |
| /// It does not check for invalid indexes such as 0 or kIndexReserved0. |
| /// |
| /// Note: This method is faster than page_to_index().paddr() as the VmPagePtr itself does not |
| /// have to be de-referenced, saving a memory load. |
| /// |
| /// # Safety |
| /// |
| /// The `index` must be a valid PMM page index. |
| pub unsafe fn index_to_paddr(&self, index: u32) -> PAddr { |
| let index = index >> Self::INDEX_ZERO_BITS; |
| let arena_ix = (index & Self::ARENA_MASK) as usize; |
| let page_ix = (index >> Self::ARENA_BITS) as usize; |
| // SAFETY: Active arenas are initialized during early boot and arena metadata is immutable. |
| unsafe { |
| let token = LockToken::new(); |
| let base = self.active_arenas(&token)[arena_ix].base().0; |
| PAddr(base + (page_ix - 1) * page::SIZE) |
| } |
| } |
| |
| /// Ends phys handoff by freeing temporary allocations. |
| pub fn end_handoff(&self) { |
| unsafe { |
| self.free_list( |
| Pin::new_unchecked(&mut *self.phys_handoff_temporary_list.get()), |
| PmmOptDelayReuse::Default, |
| ); |
| assert!((*self.phys_handoff_temporary_list.get()).is_empty()); |
| } |
| } |
| |
| /// Converts physical address to VmPagePtr if it belongs to any active arena. |
| pub fn paddr_to_page(&self, addr: PAddr) -> Option<VmPagePtr> { |
| // SAFETY: Active arenas are initialized during early boot and arena metadata is immutable. |
| let token = unsafe { LockToken::new() }; |
| for a in self.active_arenas(&token) { |
| if a.address_in_arena(addr) { |
| let index = (addr.0 - a.base().0) / page::SIZE; |
| // SAFETY: index is within arena bounds. |
| let page_ptr = unsafe { a.get_page(index) }; |
| return Some(VmPagePtr::new(page_ptr)); |
| } |
| } |
| None |
| } |
| |
| /// Allocates a single physical page from this node. |
| pub fn alloc_page(&self, alloc_flags: u32) -> Result<VmPagePtr, Status> { |
| debug_assert!(crate::kernel::thread::current_memory_allocation_state_is_enabled()); |
| let page_ptr: NonNull<VmPage>; |
| let free_list_had_fill_pattern; |
| |
| { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: lock is held. |
| free_list_had_fill_pattern = unsafe { *self.all_free_pages_filled.get() }; |
| |
| if (alloc_flags & ALLOC_FLAG_CAN_WAIT) != 0 |
| && self.should_delay_allocation_locked(token) |
| { |
| PMM_ALLOC_DELAYED.add(1); |
| return Err(Status::SHOULD_WAIT); |
| } |
| |
| // SAFETY: token proves lock is held. |
| let free_list = unsafe { self.free_list.get_mut(token) }; |
| let Some(p) = free_list.pop_front() else { |
| // Allocation failures from the regular free list are likely to become user-visible. |
| self.report_alloc_failure_locked( |
| token, |
| AllocFailure { r#type: AllocFailureType::Pmm, size: 1, free_count: 0 }, |
| ); |
| return Err(Status::NO_MEMORY); |
| }; |
| page_ptr = p; |
| |
| // SAFETY: page_ptr is a valid VmPage popped from free_list. |
| unsafe { |
| self.alloc_page_helper_locked(page_ptr); |
| } |
| self.decrement_free_count_locked(token, 1); |
| } |
| |
| let vmp = VmPagePtr::new(page_ptr); |
| if free_list_had_fill_pattern { |
| // SAFETY: vmp is a valid VmPagePtr. |
| unsafe { |
| self.checker().assert_pattern(vmp); |
| } |
| } |
| |
| Ok(vmp) |
| } |
| |
| /// Allocates `count` physical pages, adding them to the tail of `list`. |
| pub fn alloc_pages( |
| &self, |
| count: usize, |
| alloc_flags: u32, |
| mut list: Pin<&mut VmPageDoublyLinkedList>, |
| ) -> Result<(), Status> { |
| ltracef!("count {}\n", count); |
| debug_assert!(crate::kernel::thread::current_memory_allocation_state_is_enabled()); |
| if count == 0 { |
| return Ok(()); |
| } else if count == 1 { |
| let page = self.alloc_page(alloc_flags)?; |
| // SAFETY: list is pinned. |
| unsafe { |
| list.as_mut().get_unchecked_mut().push_back_raw(page.as_non_null()); |
| } |
| return Ok(()); |
| } |
| |
| let free_list_had_fill_pattern; |
| // Holds the pages that we pull out of the PMMs free list. These pages may still need to |
| // have their pattern checked (based on the bool above) before being appended to |list| and |
| // returned to the caller. |
| pin_init::stack_pin_init!(let alloc_list = DoublyLinkedList::<NonNull<VmPage>>::new()); |
| { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: lock is held. |
| free_list_had_fill_pattern = unsafe { *self.all_free_pages_filled.get() }; |
| |
| // SAFETY: token proves lock is held. |
| let free_count = unsafe { self.free_count.get(token) }.load(Ordering::Relaxed); |
| |
| if count as u64 > free_count { |
| // SAFETY: token proves lock is held. |
| if (alloc_flags & ALLOC_FLAG_CAN_WAIT) != 0 |
| && unsafe { *self.should_wait.get(token) } != ShouldWaitState::Never |
| { |
| PMM_ALLOC_DELAYED.add(1); |
| return Err(Status::SHOULD_WAIT); |
| } |
| // Allocation failures from the regular free list are likely to become user-visible. |
| self.report_alloc_failure_locked( |
| token, |
| AllocFailure { r#type: AllocFailureType::Pmm, size: count, free_count }, |
| ); |
| return Err(Status::NO_MEMORY); |
| } |
| |
| self.decrement_free_count_locked(token, count as u64); |
| |
| if (alloc_flags & ALLOC_FLAG_CAN_WAIT) != 0 |
| && self.should_delay_allocation_locked(token) |
| { |
| self.increment_free_count_locked(token, count as u64); |
| PMM_ALLOC_DELAYED.add(1); |
| return Err(Status::SHOULD_WAIT); |
| } |
| |
| // SAFETY: token proves lock is held. |
| let free_list = unsafe { self.free_list.get_mut(token) }; |
| let mut cursor = free_list.cursor_front_mut(); |
| for _ in 0..count { |
| let page = cursor.get().expect("free_count was checked"); |
| let page_ptr = NonNull::from(page); |
| // SAFETY: page_ptr is a valid VmPage pointer. |
| unsafe { |
| self.alloc_page_helper_locked(page_ptr); |
| } |
| cursor.move_next(); |
| } |
| // SAFETY: alloc_list is pinned. |
| let mut alloc_cursor = |
| unsafe { alloc_list.as_mut().get_unchecked_mut().cursor_back_mut() }; |
| cursor.split_before(&mut alloc_cursor); |
| } |
| |
| // Check the pages we are allocating before appending them into the user's allocation list. |
| // Do this check before since we must not existing pages in the user's allocation list, as |
| // they are completely arbitrary pages and there's no reason to expect a fill pattern in |
| // them. |
| if free_list_had_fill_pattern { |
| for page in alloc_list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page points to valid VmPage in list. |
| unsafe { |
| self.checker().assert_pattern(vmp); |
| } |
| } |
| } |
| |
| // Append the checked list onto the user provided list. |
| // SAFETY: Splicing into list. |
| unsafe { |
| list.as_mut().get_unchecked_mut().splice(alloc_list.as_mut().get_unchecked_mut()); |
| } |
| Ok(()) |
| } |
| |
| /// Allocates physical pages in the specific address range. |
| pub fn alloc_range( |
| &self, |
| mut address: PAddr, |
| count: usize, |
| mut list: Pin<&mut VmPageDoublyLinkedList>, |
| ) -> Result<(), Status> { |
| ltracef!("address {:#x}, count {}\n", address.0, count); |
| |
| debug_assert!(crate::kernel::thread::current_memory_allocation_state_is_enabled()); |
| // On error scenarios we will free the list, so make sure the caller didn't leave anything |
| // in there. |
| debug_assert!(list.is_empty()); |
| if count == 0 { |
| return Ok(()); |
| } |
| |
| address = PAddr(page::round_down(address.0)); |
| let mut allocated = 0usize; |
| let free_list_had_fill_pattern; |
| |
| { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: lock is held. |
| free_list_had_fill_pattern = unsafe { *self.all_free_pages_filled.get() }; |
| |
| // SAFETY: token proves lock is held. |
| let num_arenas = unsafe { *self.used_arena_count.get(token) }; |
| // walk through the arenas, looking to see if the physical page belongs to it. |
| for arena_idx in 0..num_arenas { |
| loop { |
| if allocated >= count { |
| break; |
| } |
| // SAFETY: token proves lock is held, arena_idx < num_arenas. |
| let in_arena = |
| unsafe { self.arenas.get(token) }[arena_idx].address_in_arena(address); |
| if !in_arena { |
| break; |
| } |
| // SAFETY: token proves lock is held, arena_idx < num_arenas. |
| let page_nonnull = |
| unsafe { self.arenas.get(token) }[arena_idx].find_specific(address); |
| let Some(page_nonnull) = page_nonnull else { |
| break; |
| }; |
| let page_ptr = page_nonnull; |
| |
| // As we hold lock_, we can assume that any page in the FREE state is owned by |
| // us, and protected by lock_, and so should is_free() be true we will be |
| // allowed to assume it is in the free list, remove it from said list, and |
| // allocate it. |
| // SAFETY: page_ptr is a valid VmPage in arena. |
| let page = unsafe { page_ptr.as_ref() }; |
| if !page.is_free() { |
| break; |
| } |
| |
| // We never allocate loaned pages for caller of AllocRange() |
| if page.is_loaned() { |
| break; |
| } |
| |
| // SAFETY: Reading container linkage while holding PmmNode lock. |
| debug_assert!(unsafe { (*page.queue_node.get()).in_container() }); |
| |
| // SAFETY: page is in free_list and list is pinned. |
| unsafe { |
| self.free_list.get_mut(token).erase(page); |
| self.alloc_page_helper_locked(page_ptr); |
| list.as_mut().get_unchecked_mut().push_back_raw(page_ptr); |
| } |
| allocated += 1; |
| address = PAddr(address.0 + page::SIZE); |
| self.decrement_free_count_locked(token, 1); |
| } |
| if allocated == count { |
| break; |
| } |
| } |
| if allocated < count { |
| // We were not able to allocate the entire run, free these pages. As we allocated |
| // these pages under this lock acquisition, the fill status is whatever it was |
| // before, i.e. the status of whether free pages have all been filled. No need to |
| // request delayed reuse as these pages were allocated just now, but never used. |
| // SAFETY: Freeing partially allocated pages back to node. |
| self.free_list_locked( |
| token, |
| list, |
| free_list_had_fill_pattern, |
| PmmOptDelayReuse::Default, |
| ); |
| return Err(Status::NOT_FOUND); |
| } |
| } |
| |
| if free_list_had_fill_pattern { |
| for page in list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page points to valid VmPage in list. |
| unsafe { |
| self.checker().assert_pattern(vmp); |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Allocate a run of contiguous pages, aligned on log2 byte boundary (0-31). |
| /// Return the base address of the run in the physical address pointer and |
| /// append the allocate page structures to the tail of the passed in list. |
| pub fn alloc_contiguous( |
| &self, |
| count: usize, |
| alloc_flags: u32, |
| mut alignment_log2: u8, |
| mut list: Pin<&mut VmPageDoublyLinkedList>, |
| ) -> Result<PAddr, Status> { |
| debug_assert!(crate::kernel::thread::current_memory_allocation_state_is_enabled()); |
| ltracef!("count {}, align {}\n", count, alignment_log2); |
| |
| // Forbid zero size contiguous allocations because we are obligated to provide the physical |
| // address on success, but have no sensible value to give. |
| if count == 0 { |
| return Err(Status::INVALID_ARGS); |
| } |
| if (alignment_log2 as usize) < page::SHIFT { |
| alignment_log2 = page::SHIFT as u8; |
| } |
| debug_assert!((alloc_flags & ALLOC_FLAG_CAN_WAIT) == 0); |
| |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| |
| // SAFETY: token proves lock is held. |
| let num_arenas = unsafe { *self.used_arena_count.get(token) }; |
| for arena_idx in 0..num_arenas { |
| // find_free_contiguous will search the arena for FREE pages. As we hold lock, any pages |
| // in the FREE state are assumed to be owned by us, and would only be modified if lock |
| // were held. |
| // SAFETY: token proves lock is held, arena_idx < num_arenas. |
| let p_nonnull = unsafe { self.arenas.get_mut(token) }[arena_idx] |
| .find_free_contiguous(count, alignment_log2); |
| let Some(p_nonnull) = p_nonnull else { |
| continue; |
| }; |
| let p = p_nonnull.as_ptr(); |
| |
| // SAFETY: p is a valid page pointer returned by find_free_contiguous. |
| let pa = unsafe { (*p).paddr() }; |
| |
| let mut curr_pa = pa; |
| // remove the pages from the run out of the free list. |
| for _ in 0..count { |
| // SAFETY: token proves lock is held, arena_idx < num_arenas. |
| let curr_nonnull = |
| unsafe { self.arenas.get(token) }[arena_idx].find_specific(curr_pa); |
| let Some(curr_nonnull) = curr_nonnull else { |
| panic!("arena find_specific failed on page allocated from it"); |
| }; |
| let curr_p = curr_nonnull; |
| // SAFETY: curr_p is valid VmPage in arena and was verified free. |
| unsafe { |
| let page = curr_p.as_ref(); |
| debug_assert!(page.is_free()); |
| // Loaned pages are never returned by FindFreeContiguous() above. |
| debug_assert!(!page.is_loaned()); |
| debug_assert!((*page.queue_node.get()).in_container()); |
| |
| // Atomically (that is, in a single lock acquisition) remove this page from both |
| // the free list and FREE state, ensuring it is owned by us. |
| self.free_list.get_mut(token).erase(page); |
| page.set_state(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| (*page.state_union.get()).alloc.owner = core::ptr::null_mut(); |
| |
| self.decrement_free_count_locked(token, 1); |
| asan_unpoison_page(page); |
| let vmp = VmPagePtr::new(curr_p); |
| self.checker().assert_pattern(vmp); |
| |
| list.as_mut().get_unchecked_mut().push_back_raw(curr_p); |
| } |
| curr_pa = PAddr(curr_pa.0 + page::SIZE); |
| } |
| return Ok(pa); |
| } |
| |
| // We could potentially move contents of non-pinned pages out of the way for critical |
| // contiguous allocations, but for now... |
| ltracef!("couldn't find run\n"); |
| Err(Status::NOT_FOUND) |
| } |
| |
| /// Frees a single physical page back to this node. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that page is valid and they are the owner. |
| pub unsafe fn free_page(&self, page: VmPagePtr, delay_reuse: PmmOptDelayReuse) { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| debug_assert!(unsafe { !page.is_loaned() }); |
| let fill = self.is_free_fill_enabled_racy(); |
| let page_ptr = page.as_non_null(); |
| if fill { |
| // SAFETY: page is valid. |
| unsafe { |
| self.checker().fill_pattern(page); |
| } |
| } |
| |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| |
| // SAFETY: page_ptr is valid. |
| unsafe { |
| let page_ref = page_ptr.as_ref(); |
| // pages freed individually shouldn't be in a queue. |
| debug_assert!(!(*page_ref.queue_node.get()).in_container()); |
| self.free_page_helper_locked(token, page_ptr, fill); |
| self.increment_free_count_locked(token, 1); |
| |
| if delay_reuse == PmmOptDelayReuse::Yes || cfg!(sanitize = "address") { |
| self.free_list.get_mut(token).push_back_raw(page_ptr); |
| } else { |
| self.free_list.get_mut(token).push_front_raw(page_ptr); |
| } |
| } |
| } |
| |
| /// Frees every page on `list` back to this node. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that all pages in list are valid and they are the owner. |
| pub unsafe fn free_list( |
| &self, |
| mut list: Pin<&mut VmPageDoublyLinkedList>, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| let fill = self.is_free_fill_enabled_racy(); |
| if fill { |
| for page in list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page in list is valid. |
| unsafe { |
| self.checker().fill_pattern(vmp); |
| } |
| } |
| } |
| |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| |
| self.free_list_locked(token, list.as_mut(), fill, delay_reuse); |
| } |
| |
| /// Return count of unallocated physical pages in this node. |
| pub fn count_free_pages(&self) -> u64 { |
| let token = unsafe { LockToken::new() }; |
| // SAFETY: Reading free_count. |
| unsafe { self.free_count.get(&token) }.load(Ordering::Relaxed) |
| } |
| |
| /// Return count of unallocated loaned physical pages in this node. |
| pub fn count_loaned_free_pages(&self) -> u64 { |
| let token = unsafe { LockToken::new() }; |
| unsafe { self.free_loaned_count.get(&token) }.load(Ordering::Relaxed) |
| } |
| |
| /// Return count of pages which are presently loaned with the loan cancelled. |
| pub fn count_loan_cancelled_pages(&self) -> u64 { |
| let token = unsafe { LockToken::new() }; |
| unsafe { self.loan_cancelled_count.get(&token) }.load(Ordering::Relaxed) |
| } |
| |
| /// Return count of loaned pages that are not free. |
| pub fn count_loaned_not_free_pages(&self) -> u64 { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let loaned_guard = self.loaned_list_lock.lock()); |
| let loaned_token = loaned_guard.token(); |
| ksync::lock!(let _free_guard = self.lock.lock()); |
| unsafe { self.loaned_count.get(loaned_token) }.load(Ordering::Relaxed) |
| - unsafe { self.free_loaned_count.get(loaned_token) }.load(Ordering::Relaxed) |
| } |
| |
| /// Return count of loaned pages in this node. |
| pub fn count_loaned_pages(&self) -> u64 { |
| let token = unsafe { LockToken::new() }; |
| unsafe { self.loaned_count.get(&token) }.load(Ordering::Relaxed) |
| } |
| |
| /// Return amount of physical memory in this node, in bytes. |
| pub fn count_total_bytes(&self) -> u64 { |
| let token = unsafe { LockToken::new() }; |
| // SAFETY: Reading arena_cumulative_size. |
| unsafe { *self.arena_cumulative_size.get(&token) } |
| } |
| |
| /// Enable the free fill checker with the specified fill size and action, and begin filling |
| /// freed pages (including freed loaned pages) going forward. See |PmmChecker| for definition |
| /// of fill size. |
| /// |
| /// Note, pages freed piror to calling this method will remain unfilled. To fill them, call |
| /// |FillFreePagesAndArm|. |
| /// |
| /// Returns true if the checker was enabled with the requested fill_size, or |false| otherwise. |
| pub fn enable_free_page_filling(&self, fill_size: usize, action: CheckFailAction) -> bool { |
| // Require both locks so we can manipulate free_fill_enabled. |
| ksync::lock!(let _loaned_guard = self.loaned_list_lock.lock()); |
| ksync::lock!(let _free_guard = self.lock.lock()); |
| if self.free_fill_enabled.load(Ordering::SeqCst) { |
| // Checker is already enabled. |
| return false; |
| } |
| // SAFETY: Both loaned_list_lock and lock are held, providing synchronized exclusive access |
| // to checker. |
| let checker = unsafe { &mut *self.checker.get() }; |
| checker.set_fill_size(fill_size); |
| checker.set_action(action); |
| // As free_fill_enabled may be examined outside of the lock, ensure the manipulations to |
| // checker complete first by performing a release. See is_free_fill_enabled_racy for where |
| // the acquire is performed. |
| self.free_fill_enabled.store(true, Ordering::Release); |
| true |
| } |
| |
| /// Fill all free pages (both non-loaned and loaned) with a pattern and arm the checker. See |
| /// |PmmChecker|. |
| /// |
| /// This is a no-op if the checker is not enabled. See |EnableFreePageFilling| |
| pub fn fill_free_pages_and_arm(&self) { |
| // Require both locks so we can process both of the free lists and modify |
| // all_free_pages_filled. |
| ksync::lock!(let mut loaned_guard = self.loaned_list_lock.lock()); |
| let loaned_token = loaned_guard.as_mut().token_mut(); |
| ksync::lock!(let mut free_guard = self.lock.lock()); |
| let free_token = free_guard.as_mut().token_mut(); |
| |
| if !self.free_fill_enabled.load(Ordering::SeqCst) { |
| return; |
| } |
| |
| // SAFETY: free_token proves lock is held. |
| let free_list = unsafe { self.free_list.get_mut(free_token) }; |
| for page in free_list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page in free_list is valid. |
| unsafe { |
| self.checker().fill_pattern(vmp); |
| } |
| } |
| // SAFETY: loaned_token proves lock is held. |
| let free_loaned_list = unsafe { self.free_loaned_list.get_mut(loaned_token) }; |
| for page in free_loaned_list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page in free_loaned_list is valid. |
| unsafe { |
| self.checker().fill_pattern(vmp); |
| } |
| } |
| |
| // SAFETY: Both loaned_list_lock and lock are held, providing synchronized exclusive access |
| // to checker. |
| let checker = unsafe { &mut *self.checker.get() }; |
| // Now that every page has been filled, we can arm the checker. |
| checker.arm(); |
| // SAFETY: Both loaned_list_lock and lock are held. |
| unsafe { *self.all_free_pages_filled.get() = true }; |
| checker.print_status_stdout(); |
| } |
| |
| /// Configures the free memory bounds and allows for setting a one shot signal as well as a |
| /// level where allocations should start being delayed. |
| /// |
| /// The event is signaled once the number of PMM free pages falls outside of the range given by |
| /// |free_lower_bound| and |free_upper_bound|. As the event is one shot, one signaled this must |
| /// be called again to configure a new range. If the number of free pages is already outside the |
| /// requested bound then this method fails (returns false) and no event is setup. In this case |
| /// the caller should recalculate a correct bounds and try again. |
| /// |
| /// In addition to exiting the provided memory bounds, the event will also get signaled on the |
| /// first time an allocation fails (i.e. the first time at which has_alloc_failed_no_mem would |
| /// return true). |
| /// |
| /// |delay_allocations_level| is the number of PMM free pages below which the PMM will |
| /// transition to delaying allocations that can wait, i.e. those with PMM_ALLOC_FLAG_CAN_WAIT. |
| /// This transition is sticky, and even if pages are freed to go back above this line, |
| /// allocations will remain delayed until this method is called again to re-set the level. For |
| /// this reason, and since there is only a single common Event, the |delay_allocations_level| |
| /// must either be <= the |free_lower_bound|, ensuring that the caller will have been notified |
| /// and can respond by freeing memory and/or setting a new level, or |delay_allocations_level| |
| /// can be UINT64_MAX, indicating allocations should start and remain delayed. |
| /// |
| /// # Safety |
| /// |
| /// Caller ensures that `event` lives either until this method is called again or the PmmNode is |
| /// destroyed. |
| pub unsafe fn set_free_memory_signal( |
| &self, |
| free_lower_bound: u64, |
| free_upper_bound: u64, |
| delay_allocations_pages: u64, |
| event: *mut Event, |
| ) -> bool { |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| |
| // Ensure delay allocations is valid. |
| debug_assert!( |
| delay_allocations_pages <= free_lower_bound || delay_allocations_pages == u64::MAX |
| ); |
| let free_count = self.count_free_pages(); |
| if free_count < free_lower_bound || free_count > free_upper_bound { |
| return false; |
| } |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if delay_allocations_pages == u64::MAX { |
| self.trip_free_pages_level_locked(token); |
| } else if *self.should_wait.get(token) == ShouldWaitState::UntilReset { |
| self.may_allocate_evt.signal(); |
| *self.should_wait.get_mut(token) = ShouldWaitState::OnceLevelTripped; |
| } |
| *self.should_wait_free_pages_level.get_mut(token) = delay_allocations_pages; |
| *self.mem_signal_lower_bound.get_mut(token) = free_lower_bound; |
| *self.mem_signal_upper_bound.get_mut(token) = free_upper_bound; |
| *self.mem_signal.get_mut(token) = event; |
| } |
| true |
| } |
| |
| /// Waits the system to exit low memory state and then attempts to allocate. |
| /// |
| /// To prevent herding problem, and because allocation compete for the `PmmNode::lock_` anyway, |
| /// only one thread is woken up a time, only if the previous thread successfully allocated. |
| /// |
| /// In normal conditions, when the system is in low memory state, this method will return |
| /// `ZX_ERR_TIMED_OUT` if the system didn't transition fast enough. If we run into a TOC to TOU, |
| /// for the system race `ZX_ERR_SHOULD_WAIT` will be returned, that is the system transitioned |
| /// out and back into low memory state before we managed to perform the allocation. |
| /// |
| /// If `BootOptions::Get()->pmm_alloc_random_wait` is true, then the system |
| /// may return spurious `ZX_ERR_SHOULD_WAIT`, in such cases, if the system is |
| /// not in a low memory state, a thread is woken up anyway, so forward |
| /// progress can be made. |
| /// |
| /// If |suspendable| is true, the wait will terminate early with |
| /// `ZX_ERR_INTERNAL_INTR_RETRY` if the thread is suspended. If false, suspension is ignored and |
| /// the wait continues. |
| pub fn wait_for_single_page_allocation( |
| &self, |
| deadline: Deadline, |
| suspendable: bool, |
| ) -> Result<VmPagePtr, Status> { |
| let mut skip_wait = false; |
| { |
| ksync::lock!(let guard = self.lock.lock()); |
| let token = guard.token(); |
| // If we have been instructed to never wait, skip waiting on the event entirely to |
| // prevent blocking. The Never state is final. |
| // SAFETY: token proves lock is held. |
| if unsafe { *self.should_wait.get(token) } == ShouldWaitState::Never { |
| skip_wait = true; |
| } |
| } |
| |
| if !skip_wait { |
| // Ignore the suspend signal if not suspendable, and retry the wait if interrupted. |
| let signal_mask = if suspendable { 0 } else { THREAD_SIGNAL_SUSPEND }; |
| let mut wait_result; |
| loop { |
| wait_result = self.may_allocate_evt.wait_mask(&deadline, signal_mask); |
| if wait_result == Err(Status::INTERRUPTED_RETRY) && !suspendable { |
| continue; |
| } |
| break; |
| } |
| |
| // Let the caller handle the error and retry if necessary. |
| // This could be `ZX_ERR_TIMED_OUT`, `ZX_ERR_INTERNAL_INTR_KILLED` (thread killed) |
| // or `ZX_ERR_INTERNAL_INTR_RETRY` (thread suspended, if suspendable is true). |
| wait_result?; |
| } |
| |
| // Try to allocate the page now, it may fail sporadically, since there is no guarantee that |
| // by the time we attempt to allocate the pages are still available. |
| let res = self.alloc_page(ALLOC_FLAG_CAN_WAIT); |
| |
| // Normally we would only signal in the `ZX_OK` case, i.e. when we are in an allocation-able |
| // state. Otherwise we would wake up another thread just for it to receive |
| // `ZX_ERR_SHOULD_WAIT` and immediately go back to waiting on the event. However, we also |
| // signal in these cases: |
| // |
| // 1) `should_wait_ == Never`: We unconditionally signal to ensure all waiting threads are |
| // woken up (cascading signal) and no new threads block. |
| // |
| // 2) `pmm_alloc_random_should_wait`: In the random wait mode, we may block in a non low |
| // memory state, which will lead to threads getting blocked, and no one kicking them out. |
| // The unblocking chain is triggered by the system moving OUT of a low memory state, |
| // which would signal the event. To avoid checking the boot option explicitly, we always |
| // signal if the allocation returned `ZX_ERR_SHOULD_WAIT` and we are not in the |
| // `UntilReset` state, which leads to one extra spurious wake-up. |
| let mut should_signal = false; |
| { |
| ksync::lock!(let guard = self.lock.lock()); |
| let token = guard.token(); |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if *self.should_wait.get(token) == ShouldWaitState::Never { |
| should_signal = true; |
| } else if *self.should_wait.get(token) != ShouldWaitState::UntilReset { |
| should_signal = res.is_ok() || res == Err(Status::SHOULD_WAIT); |
| } |
| } |
| } |
| |
| if should_signal { |
| self.may_allocate_evt.signal(); |
| } |
| |
| res |
| } |
| |
| /// Tells the node to stop returning SHOULD_WAIT. |
| pub fn stop_returning_should_wait(&self) { |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: token proves lock is held. |
| unsafe { |
| *self.should_wait.get_mut(token) = ShouldWaitState::Never; |
| } |
| self.may_allocate_evt.signal(); |
| } |
| |
| /// Returns whether an allocation has failed with NO_MEMORY. |
| pub fn has_alloc_failed_no_mem(&self) -> bool { |
| self.alloc_failed_no_mem.load(Ordering::Relaxed) |
| } |
| |
| /// Retrieves information from the first allocation failure. |
| pub fn get_first_alloc_failure(&self) -> AllocFailure { |
| ksync::lock!(let guard = self.lock.lock()); |
| let token = guard.token(); |
| // SAFETY: token proves lock is held. |
| unsafe { *self.first_alloc_failure.get(token) } |
| } |
| |
| /// This method should be called when the PMM fails to allocate in a user-visible way and will |
| /// (optionally) trigger an asynchronous OOM response. |
| pub fn report_alloc_failure(&self, failure: AllocFailure) { |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| self.report_alloc_failure_locked(token, failure); |
| } |
| |
| /// Frees all pages in the given list and places them in the loaned state available to be |
| /// returned from AllocLoanedPage. |
| /// |
| /// |delay_reuse| controls whether the newly loaned pages are eligile for immediate or delayed |
| /// reuse. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that all pages in list are valid and they are the owner. |
| pub unsafe fn begin_loan( |
| &self, |
| list: Pin<&mut VmPageDoublyLinkedList>, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| let fill = self.is_free_fill_enabled_racy(); |
| if fill { |
| for page in list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page in list is valid. |
| unsafe { |
| self.checker().fill_pattern(vmp); |
| } |
| } |
| } |
| |
| ksync::lock!(let mut guard = self.loaned_list_lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| |
| let mut loaned_count = 0u64; |
| for page in list.iter() { |
| debug_assert!(!page.is_loaned()); |
| debug_assert!(!page.is_free()); |
| page.set_is_loaned(); |
| loaned_count += 1; |
| debug_assert!(!page.is_loan_cancelled()); |
| } |
| |
| self.increment_loaned_count_locked(token, loaned_count); |
| // Callers of begin_loan() generally won't want the pages loaned to them; the intent is to |
| // loan to the rest of the system, so go ahead and free also. Some callers will basically |
| // choose between pmm_begin_loan() and pmm_free(). |
| // SAFETY: Modifying list under lock. |
| unsafe { |
| self.free_loaned_list_locked( |
| token, |
| list.get_unchecked_mut(), |
| fill, |
| delay_reuse, |
| |_| {}, |
| ); |
| } |
| } |
| |
| /// Marks a page that had been previously provided to BeginLoan as cancelled. This page may be |
| /// in the FREE_LOANED state, or presently in use. |
| /// |
| /// This call prevents the page from being reused for any new purpose until EndLoan(). For |
| /// presently-FREE_LOANED pages, this removes the pages from free_loaned_list_. For |
| /// presently-used pages, this specifies that the page will not be added to free_loaned_list_ |
| /// when later freed. Once this page is FREE_LOANED (to be ensured by the caller via |
| /// PhysicalPageProvider reclaim of the pages), the loan can be ended with EndLoan(). |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that page is valid |
| pub unsafe fn cancel_loan(&self, page: VmPagePtr) { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| // Require both locks in order to iterate the arenas and manipulate the loaned list. |
| ksync::lock!(let mut loaned_guard = self.loaned_list_lock.lock()); |
| let loaned_token = loaned_guard.as_mut().token_mut(); |
| ksync::lock!(let _arena_guard = self.lock.lock()); |
| |
| let page_raw = page.as_raw(); |
| // SAFETY: page is a valid VmPagePtr. |
| unsafe { |
| let page_ref = &*page_raw; |
| debug_assert!(page_ref.is_loaned()); |
| debug_assert!(!page_ref.is_free()); |
| // We can assert this because of PageSource's overlapping request handling. |
| let was_cancelled = page_ref.is_loan_cancelled(); |
| debug_assert!(!was_cancelled); |
| page_ref.set_is_loan_cancelled(); |
| self.increment_loan_cancelled_count_locked(loaned_token, 1); |
| if page_ref.is_free_loaned() { |
| // Currently in free_loaned_list. |
| debug_assert!((*page_ref.queue_node.get()).in_container()); |
| // Remove from free_loaned_list to prevent any new use until after end_loan. |
| self.free_loaned_list.get_mut(loaned_token).erase(page_ref); |
| self.decrement_free_loaned_count_locked(loaned_token, 1); |
| } |
| } |
| } |
| |
| /// Allocates the page to the caller as a regular non-loaned page. Must currently be: |
| /// * Loaned (via BeginLoan). |
| /// * Have had its loan cancelled (via CancelLoan). |
| /// * Be in the FREE_LOANED state. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that page is valid |
| pub unsafe fn end_loan(&self, page: VmPagePtr) { |
| let free_list_had_fill_pattern; |
| let page_ptr = page.as_non_null(); |
| |
| { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| // Require both locks in order to manipulate loaned pages and the regular free list. |
| ksync::lock!(let mut loaned_guard = self.loaned_list_lock.lock()); |
| let loaned_token = loaned_guard.as_mut().token_mut(); |
| ksync::lock!(let _free_guard = self.lock.lock()); |
| |
| // SAFETY: loaned_list_lock and lock are held. |
| free_list_had_fill_pattern = unsafe { *self.all_free_pages_filled.get() }; |
| |
| // SAFETY: page is a valid VmPagePtr. |
| unsafe { |
| let page_ref = page_ptr.as_ref(); |
| // PageSource serializing such that there's only one request to |
| // PageProvider in flight at a time for any given page is the main |
| // reason we can assert these instead of needing to check these. |
| debug_assert!(page_ref.is_loaned()); |
| debug_assert!(page_ref.is_loan_cancelled()); |
| debug_assert!(page_ref.is_free_loaned()); |
| // Already not in free_loaned_list_ (because loan_cancelled |
| // already). |
| debug_assert!(!(*page_ref.queue_node.get()).in_container()); |
| |
| page_ref.clear_is_loaned(); |
| page_ref.clear_is_loan_cancelled(); |
| // Change the state to regular FREE. When this page was made |
| // FREE_LOANED all of the pmm checker filling and asan work was |
| // done, so we are safe to just change the state without using a |
| // helper. |
| page_ref.set_state(VmPageState(page_bindings::vm_page_state::FREE)); |
| |
| self.alloc_page_helper_locked(page_ptr); |
| |
| self.decrement_loan_cancelled_count_locked(loaned_token, 1); |
| self.decrement_loaned_count_locked(loaned_token, 1); |
| } |
| } |
| |
| if free_list_had_fill_pattern { |
| // SAFETY: page is a valid VmPagePtr. |
| unsafe { |
| self.checker().assert_pattern(page); |
| } |
| } |
| } |
| |
| /// Allocates a single page from the loaned pages list. The allocated page will always have |
| /// is_loaned() being true, and must be returned by either FreeLoanedPage or FreeLoanedList. If |
| /// there are not loaned pages available ZX_ERR_UNAVAILABLE is returned, as an absence of loaned |
| /// pages does not constitute an out of memory scenario. |
| /// The provided callback must transition the page into a state such that it has a valid |
| /// backlink, i.e. it is in the OBJECT state with an owner set, prior to returning. |
| /// During the execution of the callback the page contents must *not* be modified. |
| pub fn alloc_loaned_page<F: FnOnce(VmPagePtr)>( |
| &self, |
| allocated: F, |
| ) -> Result<VmPagePtr, Status> { |
| debug_assert!(crate::kernel::thread::current_memory_allocation_state_is_enabled()); |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| let free_list_had_fill_pattern; |
| let page_ptr: NonNull<VmPage>; |
| |
| { |
| ksync::lock!(let mut guard = self.loaned_list_lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: loaned_list_lock is held. |
| free_list_had_fill_pattern = unsafe { *self.all_free_pages_filled.get() }; |
| |
| // SAFETY: token proves lock is held. |
| let free_loaned_list = unsafe { self.free_loaned_list.get_mut(token) }; |
| let Some(p) = free_loaned_list.pop_front() else { |
| // Does not count as out of memory, so do not report an allocation failure, just |
| // tell the caller we are out of resources. |
| return Err(Status::NO_RESOURCES); |
| }; |
| page_ptr = p; |
| |
| // SAFETY: page_ptr is a valid VmPage in free_loaned_list. |
| unsafe { |
| self.alloc_loaned_page_helper_locked(page_ptr); |
| self.decrement_free_loaned_count_locked(token, 1); |
| let page_vm = VmPagePtr::new(page_ptr); |
| // Run the callback while still holding the lock. |
| allocated(page_vm); |
| let page_ref = page_ptr.as_ref(); |
| // Before we drop the loaned list lock the page is expected to be in the object |
| // state with a back pointer. |
| debug_assert!( |
| page_ref.state() == VmPageState(page_bindings::vm_page_state::OBJECT) |
| && !page_ref.get_object().is_null() |
| ); |
| } |
| } |
| |
| let vmp = VmPagePtr::new(page_ptr); |
| if free_list_had_fill_pattern { |
| // SAFETY: vmp is a valid VmPagePtr. |
| unsafe { |
| self.checker().assert_pattern(vmp); |
| } |
| } |
| |
| Ok(vmp) |
| } |
| |
| /// Begins freeing a loaned page that was previously allocated by AllocLoanPage by moving into a |
| /// holding object. It is an error to attempt to free a non loaned page. When this method is |
| /// called the |page| must have a valid backlink (i.e. be in the OBJECT state with an owner |
| /// set). This backlink should be removed by the |release_page| callback, which is invoked |
| /// under the loaned pages lock, prior to transition the page into the holding state. The caller |
| /// *must*, at some point in the future, complete the page freeing process by passing the |
| /// provided |flph| into a |FinishFreeLoanedPages| call. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that page is valid, loaned and owned by them. |
| pub unsafe fn begin_free_loaned_page<F: FnOnce(VmPagePtr)>( |
| &self, |
| page: VmPagePtr, |
| release_page: F, |
| mut flph: Pin<&mut FreeLoanedPagesHolder>, |
| ) { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| debug_assert!(unsafe { page.is_loaned() }); |
| let page_ptr = page.as_non_null(); |
| // SAFETY: caller guarantees page is valid and owned by them. |
| unsafe { |
| let page_ref = page_ptr.as_ref(); |
| // On entry we require that the page has a valid backlink. |
| debug_assert!( |
| page_ref.state() == VmPageState(page_bindings::vm_page_state::OBJECT) |
| && !page_ref.get_object().is_null() |
| ); |
| } |
| |
| ksync::lock!(let guard = self.loaned_list_lock.lock()); |
| let _token = guard.token(); |
| |
| release_page(page); |
| |
| // SAFETY: caller guarantees valid page. |
| unsafe { |
| let p = page_ptr.as_ref(); |
| // pages freed individually shouldn't be in a queue. |
| debug_assert!(!(*p.queue_node.get()).in_container()); |
| debug_assert!(!flph.used); |
| p.set_state(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| (*p.state_union.get()).alloc.owner = |
| (flph.as_mut().get_unchecked_mut() as *mut FreeLoanedPagesHolder).cast(); |
| flph.as_mut().get_unchecked_mut().pages.push_front_raw(page_ptr); |
| } |
| } |
| |
| /// Completes the freeing of any loaned pages in |flph|, after which |flph| is allowed to be |
| /// destructed. Once this method is called on a given |flph| that object is effectively 'dead' |
| /// and is not allowed to be passed to any PmmNode methods. |
| pub fn finish_free_loaned_pages(&self, mut flph: Pin<&mut FreeLoanedPagesHolder>) { |
| if flph.pages.is_empty() { |
| return; |
| } |
| |
| let fill = self.is_free_fill_enabled_racy(); |
| if fill { |
| for page in flph.pages.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: valid page in flph. |
| unsafe { |
| self.checker().fill_pattern(vmp); |
| } |
| } |
| } |
| |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let mut guard = self.loaned_list_lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: Modifying flph under lock. |
| let flph_mut = unsafe { flph.as_mut().get_unchecked_mut() }; |
| debug_assert!(!flph_mut.used); |
| flph_mut.used = true; |
| |
| // Why default and not "yes"? The primary reason to delay reuse is to mitigate "bad DMA" |
| // involving previously pinned pages. Because the pages we're about to free had been on |
| // loan and because we do not pin loaned pages (see |
| // |VmCowPages::ReplacePagesWithNonLoanedLocked|) we have no reason to delay their reuse. |
| let delay_reuse = PmmOptDelayReuse::Default; |
| |
| let expected_owner = (flph_mut as *mut FreeLoanedPagesHolder).cast(); |
| self.free_loaned_list_locked(token, &mut flph_mut.pages, fill, delay_reuse, |page| { |
| debug_assert!(page.state() == VmPageState(page_bindings::vm_page_state::ALLOC)); |
| // SAFETY: page is in ALLOC state with owner. |
| unsafe { |
| debug_assert!((*page.state_union.get()).alloc.owner == expected_owner); |
| (*page.state_union.get()).alloc.owner = core::ptr::null_mut(); |
| } |
| }); |
| |
| // With the pager owners all cleared, no more waiters can come along so we can wake all the |
| // existing ones up. |
| while let Some(waiter_ptr) = flph_mut.waiters.pop_front() { |
| // SAFETY: waiter_ptr is a valid FreeLoanedPagesHolderWaiter on waiter thread stack. |
| unsafe { |
| (*waiter_ptr).event.signal(); |
| } |
| } |
| } |
| |
| /// Add new pages to the free queue. Used when bootstrapping a PmmArena. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that all pages in |list| are valid and owned by them. |
| pub unsafe fn add_free_pages(&mut self, mut list: Pin<&mut VmPageDoublyLinkedList>) { |
| ltracef!("list {:p}\n", list.as_ref().get_ref() as *const _); |
| |
| // SAFETY: called at boot time as arenas are brought online, no locks are acquired |
| let mut token = unsafe { LockToken::new() }; |
| |
| let mut free_count = 0u64; |
| // SAFETY: pop_front does not move data outside the list. |
| while let Some(page) = unsafe { list.as_mut().get_unchecked_mut().pop_front() } { |
| // SAFETY: `page` comes from valid VmPage array. |
| unsafe { |
| debug_assert!(!page.as_ref().is_loaned()); |
| debug_assert!(!page.as_ref().is_loan_cancelled()); |
| debug_assert!(page.as_ref().is_free()); |
| self.free_list.get_mut(&mut token).push_back_raw(page); |
| } |
| free_count += 1; |
| } |
| // SAFETY: called at boot time as arenas are brought online. |
| unsafe { self.free_count.get(&token) }.fetch_add(free_count, Ordering::Relaxed); |
| assert!(unsafe { self.free_count.get(&token) }.load(Ordering::Relaxed) != 0); |
| self.may_allocate_evt.signal(); |
| |
| ltracef!( |
| "free count now {}\n", |
| unsafe { self.free_count.get(&token) }.load(Ordering::Relaxed) |
| ); |
| } |
| |
| /// Retrieve access to the page queues. |
| pub fn page_queues(&self) -> &PageQueues { |
| // SAFETY: page_queues is valid for the lifetime of PmmNode. |
| unsafe { &*self.page_queues.get() } |
| } |
| |
| fn free_list_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLockClass>, |
| list: Pin<&mut VmPageDoublyLinkedList>, |
| already_filled: bool, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| let mut count = 0u64; |
| for page in list.iter() { |
| let page_ptr = NonNull::from(page); |
| debug_assert!(!page.is_loaned()); |
| // SAFETY: page_ptr is valid. |
| unsafe { |
| self.free_page_helper_locked(token, page_ptr, already_filled); |
| } |
| count += 1; |
| } |
| |
| // SAFETY: token proves lock is held. |
| unsafe { |
| return_pages_to_free_list( |
| self.free_list.get_mut(token), |
| list.get_unchecked_mut(), |
| delay_reuse, |
| ); |
| } |
| |
| self.increment_free_count_locked(token, count); |
| } |
| |
| /// Calls the provided function, passing |page| back into it, serialized with any other calls to |
| /// |alloc_loaned_page|, |begin_free_loaned_page| and |finish_free_loaned_pages|. This allows |
| /// caller to know that while the |with_page| callback is running there are no in progress calls |
| /// to these methods, and that the page is not presently in holding object, i.e. it is either |
| /// fully owned by the PmmNode, or fully owned by an object. |
| pub fn with_loaned_page<F: FnOnce(VmPagePtr)>(&self, page: VmPagePtr, with_page: F) { |
| // Technically users could race with |with_loaned_page| and re-allocate the page after it |
| // gets migrated to the PmmNode, and then place it back in a new FLPH before a stable state |
| // can be observed. Such behavior almost certainly represents a kernel bug, so if we detect |
| // multiple iterations to track the page down we generate a warning. |
| let mut iterations: u32 = 0; |
| loop { |
| // Intentionally allocate a new waiter every iteration so that its destructor can detect |
| // if it has been left in a list incorrectly between iterations. |
| pin_init::stack_pin_init!(let waiter = pin_init::pin_init!(FreeLoanedPagesHolderWaiter { |
| node: SinglyLinkedListNode::new(), |
| event <- Event::init_unsignaled(), |
| })); |
| { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| ksync::lock!(let guard = self.loaned_list_lock.lock()); |
| unsafe { |
| debug_assert!(page.is_loaned()); |
| if page.state() != VmPageState(page_bindings::vm_page_state::ALLOC) |
| || (*page.as_ref().state_union.get()).alloc.owner.is_null() |
| { |
| with_page(page); |
| return; |
| } |
| let flph_ptr = (*page.as_ref().state_union.get()) |
| .alloc |
| .owner |
| .cast::<FreeLoanedPagesHolder>(); |
| (*flph_ptr).waiters.push_front_raw(waiter.as_ref().get_ref() |
| as *const FreeLoanedPagesHolderWaiter |
| as *mut FreeLoanedPagesHolderWaiter); |
| } |
| // After placing waiter in the list and dropping the loaned_list_lock_ we must not |
| // manipulate the intrusive list node in the Waiter, as it is now owned by the FLPH. |
| } |
| if iterations > 0 { |
| kprint::kprintln!( |
| "WARNING: Required multiple attempts {iterations} to track down loaned page \ |
| {:p}", |
| page.as_raw() |
| ); |
| } |
| |
| // Now that the lock is dropped, wait on the event. |
| let _ = waiter.event.wait_infinite(); |
| |
| // Grab the loaned_list_lock to ensure that the finished_loaned_free_pages path has |
| // finished holding any reference to our event. |
| ksync::lock!(let guard = self.loaned_list_lock.lock()); |
| |
| iterations = iterations.wrapping_add(1); |
| } |
| } |
| |
| /// Begins freeing multiple pages that were allocated by |alloc_loaned_page| by moving into a |
| /// holding object. It is an error to attempt to free any non loaned pages. When this method is |
| /// called all pages in the array must have a valid backlink (i.e. be in the OBJECT state with |
| /// an owner set), and the |release_list| method must remove the backlink from all pages, and |
| /// place them in the provided list in the same order. |
| /// The caller *must*, at some point in the future, complete the page freeing process by passing |
| /// the provided |flph| into a |finish_free_loaned_pages| call. |
| /// |
| /// # Safety |
| /// |
| /// Caller guarantees that all |pages| are valid, loaned and owned by them. |
| pub unsafe fn begin_free_loaned_array< |
| F: FnOnce(&[VmPagePtr], Pin<&mut VmPageDoublyLinkedList>), |
| >( |
| &self, |
| pages: &[VmPagePtr], |
| release_list: F, |
| mut flph: Pin<&mut FreeLoanedPagesHolder>, |
| ) { |
| let _preempt_disable = AutoPreemptDisabler::new(); |
| // On entry we expect all pages to have a backlink. |
| for p in pages { |
| // SAFETY: valid pages slice. |
| unsafe { |
| let page_ref = &*p.as_raw(); |
| debug_assert!( |
| page_ref.state() == VmPageState(page_bindings::vm_page_state::OBJECT) |
| && !page_ref.get_object().is_null() |
| ); |
| } |
| } |
| |
| ksync::lock!(let guard = self.loaned_list_lock.lock()); |
| let _token = guard.token(); |
| debug_assert!(!flph.used); |
| |
| pin_init::stack_pin_init!(let free_list = DoublyLinkedList::<NonNull<VmPage>>::new()); |
| release_list(pages, free_list.as_mut()); |
| |
| // Validate that the callback populated the free list correctly. |
| let mut expected = 0usize; |
| // SAFETY: free_list is pinned on stack and exclusively accessed. |
| for p in unsafe { free_list.as_mut().get_unchecked_mut().iter() } { |
| // SAFETY: p is in ALLOC state and we hold loaned_list_lock. |
| unsafe { |
| p.set_state(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| (*p.state_union.get()).alloc.owner = |
| (flph.as_mut().get_unchecked_mut() as *mut FreeLoanedPagesHolder).cast(); |
| } |
| debug_assert!(pages[expected].as_non_null() == NonNull::from(p)); |
| expected += 1; |
| } |
| debug_assert_eq!(expected, pages.len()); |
| |
| // SAFETY: Splicing into flph.pages. |
| unsafe { |
| flph.as_mut() |
| .get_unchecked_mut() |
| .pages |
| .cursor_front_mut() |
| .splice(free_list.as_mut().get_unchecked_mut()); |
| } |
| } |
| |
| fn free_loaned_list_locked<F: FnMut(&VmPage)>( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| list: &mut VmPageDoublyLinkedList, |
| already_filled: bool, |
| delay_reuse: PmmOptDelayReuse, |
| mut validator: F, |
| ) { |
| let mut count = 0u64; |
| let mut cursor = list.cursor_front_mut(); |
| while let Some(page) = cursor.get() { |
| let page_ptr = NonNull::from(page); |
| validator(page); |
| // SAFETY: page_ptr is valid. |
| unsafe { |
| debug_assert!(page.is_loaned()); |
| self.free_loaned_page_helper_locked(token, page_ptr, already_filled); |
| if page.is_loan_cancelled() { |
| // Loaned cancelled pages do not go back on the free list. |
| cursor.erase(); |
| } else { |
| count += 1; |
| cursor.move_next(); |
| } |
| } |
| } |
| |
| // SAFETY: token proves lock is held. |
| unsafe { |
| return_pages_to_free_list(self.free_loaned_list.get_mut(token), list, delay_reuse); |
| } |
| |
| self.increment_free_loaned_count_locked(token, count); |
| } |
| |
| /// Unwires a page that was previously in WIRED state. |
| pub fn unwire_page(&self, page: VmPagePtr) { |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: page is a valid VmPagePtr. |
| unsafe { |
| let page_ref = &*page.as_raw(); |
| assert_eq!(page_ref.state(), VmPageState(page_bindings::vm_page_state::WIRED)); |
| if (*page_ref.queue_node.get()).in_container() { |
| self.phys_handoff_vmo_list.get_mut(token).erase(page_ref); |
| } |
| page_ref.set_state(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| } |
| } |
| |
| /// Returns the number of active arenas. |
| pub fn num_arenas(&self) -> usize { |
| ksync::lock!(let guard = self.lock.lock()); |
| let token = guard.token(); |
| // SAFETY: token proves lock is held. |
| unsafe { *self.used_arena_count.get(token) } |
| } |
| |
| /// Fills |buffer| with PmmArenaInfo objects starting at |offset| arena, ordered by base |
| /// address. For example, passing an |offset| of 1 would skip the 1st arena. |
| /// |
| /// Returns OUT_OF_RANGE if |offset| would yield an invalid. |
| pub fn get_arena_info<'a>( |
| &self, |
| offset: usize, |
| buffer: &'a mut [MaybeUninit<PmmArenaInfo>], |
| ) -> Result<&'a mut [PmmArenaInfo], Status> { |
| ksync::lock!(let guard = self.lock.lock()); |
| let token = guard.token(); |
| let active = self.active_arenas(token); |
| let active_len = active.len(); |
| |
| if buffer.is_empty() || (offset >= active_len) { |
| return Err(Status::OUT_OF_RANGE); |
| } |
| |
| let count = (active_len - offset).min(buffer.len()); |
| let buffer = &mut buffer[0..count]; |
| |
| // Skip the first |offset| elements and copy the next elements. |
| for off in 0..buffer.len() { |
| unsafe { |
| buffer[off].as_mut_ptr().write(active[off + offset].info().clone()); |
| } |
| } |
| // SAFETY: we have fully initialized the buffer. |
| unsafe { Ok(buffer.assume_init_mut()) } |
| } |
| |
| /// Prints free megabytes to stdout. |
| pub fn dump_free(&self) { |
| const MB: u64 = 1024 * 1024; |
| let megabytes_free = self.count_free_pages() * (page::SIZE as u64) / MB; |
| kprint::kprintln!(" {} free MBs", megabytes_free); |
| } |
| |
| /// Dumps arena and page state diagnostics. |
| pub fn dump(&self, is_panic: bool) { |
| // No lock analysis here, as we want to just go for it in the panic case without the lock. |
| let dump_inner = |token: &LockToken<'_, PmmNodeLockClass>| { |
| // SAFETY: token proves lock is held or synthesized in panic. |
| let free_count = unsafe { self.free_count.get(token) }.load(Ordering::Relaxed); |
| let loaned_token = unsafe { LockToken::new() }; |
| let free_loaned_count = |
| unsafe { self.free_loaned_count.get(&loaned_token) }.load(Ordering::Relaxed); |
| // SAFETY: token proves lock is held or synthesized in panic. |
| let total_size = unsafe { *self.arena_cumulative_size.get(token) }; |
| kprint::kprintln!( |
| "pmm node {:p}: free_count {} ({} bytes), free_loaned_count: {} ({} bytes), total \ |
| size {}\n", |
| self as *const _, |
| free_count, |
| free_count * (page::SIZE as u64), |
| free_loaned_count, |
| free_loaned_count * (page::SIZE as u64), |
| total_size |
| ); |
| let mut count_sum = PmmStateCount::default(); |
| for a in self.active_arenas(token) { |
| a.dump(false, false, Some(&mut count_sum)); |
| } |
| kprint::kprintln!("Totals\n"); |
| print_page_state_counts(&count_sum); |
| }; |
| |
| if is_panic { |
| // SAFETY: In panic context, synthesize lock token. |
| let token = unsafe { LockToken::new() }; |
| dump_inner(&token); |
| } else { |
| ksync::lock!(let guard = self.lock.lock()); |
| let token = guard.token(); |
| dump_inner(token); |
| } |
| } |
| |
| /// Retrieve any page compression instance. If this returns non-null then it's return value will |
| /// not change and the result can be cached. |
| pub fn get_page_compression(&self) -> Option<&VmCompression> { |
| ksync::lock!(let guard = self.compression_lock.lock()); |
| let token = guard.token(); |
| // SAFETY: token proves lock is held. Once `page_compression` is set to `Some`, it is never |
| // modified or cleared for the lifetime of `self`. |
| unsafe { self.page_compression.get(token).as_ref().map(|ptr| &*fbl::RefPtr::as_ptr(ptr)) } |
| } |
| |
| /// Set the page compression instance. Returns an error if one has already been set. |
| pub fn set_page_compression( |
| &self, |
| compression: fbl::RefPtr<VmCompression>, |
| ) -> Result<(), Status> { |
| ksync::lock!(let mut guard = self.compression_lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if self.page_compression.get(token).is_some() { |
| return Err(Status::ALREADY_EXISTS); |
| } |
| *self.page_compression.get_mut(token) = Some(compression); |
| } |
| Ok(()) |
| } |
| |
| /// Returns a reference to the evictor. |
| pub fn evictor(&self) -> &Evictor { |
| // SAFETY: evictor is valid for the lifetime of PmmNode. |
| unsafe { &*self.evictor.get() } |
| } |
| |
| /// Returns a reference to the free fill checker. |
| pub fn checker(&self) -> &PmmChecker { |
| // SAFETY: checker is valid for the lifetime of PmmNode. |
| unsafe { &*self.checker.get() } |
| } |
| |
| /// Returns the global failed allocation count across all CPUs. |
| pub fn get_alloc_failed_count() -> i64 { |
| PMM_ALLOC_FAILED.sum_across_all_cpus() |
| } |
| |
| /// If randomly waiting on allocations is enabled, this re-seeds from the global prng, otherwise |
| /// it does nothing. |
| pub fn seed_random_should_wait(&self) { |
| if cfg!(debug_assertions) { |
| ksync::lock!(let mut guard = self.lock.lock()); |
| let token = guard.as_mut().token_mut(); |
| // SAFETY: cpp_global_prng_draw writes size bytes into buffer. |
| unsafe { |
| cpp_global_prng_draw( |
| (self.random_should_wait_seed.get_mut(token) as *mut usize).cast(), |
| core::mem::size_of::<usize>(), |
| ); |
| } |
| } |
| } |
| |
| /// Synchronously walk the PMM's free list (and free loaned list) and validate each page. This |
| /// is an incredibly expensive operation and should only be used for debugging purposes. |
| pub fn check_all_free_pages(&self) { |
| // Require both locks so we can process both of the free lists. This is an infrequent manual |
| // operation and does not need to be optimized to avoid holding both locks at once. |
| ksync::lock!(let mut loaned_guard = self.loaned_list_lock.lock()); |
| let loaned_token = loaned_guard.as_mut().token_mut(); |
| ksync::lock!(let mut free_guard = self.lock.lock()); |
| let free_token = free_guard.as_mut().token_mut(); |
| |
| if !self.checker().is_armed() { |
| return; |
| } |
| |
| let mut free_page_count = 0u64; |
| let mut free_loaned_page_count = 0u64; |
| |
| dprintf!(INFO, "PMM: checking free list...\n"); |
| // SAFETY: free_token proves lock is held. |
| let free_list = unsafe { self.free_list.get_mut(free_token) }; |
| for page in free_list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page in free_list is valid. |
| unsafe { |
| self.checker().assert_pattern(vmp); |
| } |
| free_page_count += 1; |
| } |
| dprintf!(INFO, "PMM: done checking free list\n"); |
| |
| dprintf!(INFO, "PMM: checking free loaned list...\n"); |
| // SAFETY: loaned_token proves lock is held. |
| let free_loaned_list = unsafe { self.free_loaned_list.get_mut(loaned_token) }; |
| for page in free_loaned_list.iter() { |
| let vmp = VmPagePtr::new(NonNull::from(page)); |
| // SAFETY: page in free_loaned_list is valid. |
| unsafe { |
| self.checker().assert_pattern(vmp); |
| } |
| free_loaned_page_count += 1; |
| } |
| dprintf!(INFO, "PMM: done checking free loaned list\n"); |
| |
| // SAFETY: free_token proves lock is held. |
| assert_eq!( |
| free_page_count, |
| unsafe { self.free_count.get(free_token) }.load(Ordering::Relaxed) |
| ); |
| assert_eq!( |
| free_loaned_page_count, |
| unsafe { self.free_loaned_count.get(loaned_token) }.load(Ordering::Relaxed) |
| ); |
| } |
| |
| /// Synchronously walk the PMM's free list (and free loaned list) and poison each page. |
| #[cfg(sanitize = "address")] |
| pub fn poison_all_free_pages(&self) { |
| // Require both locks so we can process both of the free lists. This is an infrequent manual |
| // operation and does not need to be optimized to avoid holding both locks at once. |
| ksync::lock!(let mut loaned_guard = self.loaned_list_lock.lock()); |
| let loaned_token = loaned_guard.as_mut().token_mut(); |
| ksync::lock!(let mut free_guard = self.lock.lock()); |
| let free_token = free_guard.as_mut().token_mut(); |
| |
| // SAFETY: free_token proves lock is held. |
| let free_list = unsafe { self.free_list.get_mut(free_token) }; |
| for page in free_list.iter() { |
| asan_poison_page(page, ASAN_PMM_FREE_MAGIC); |
| } |
| // SAFETY: loaned_token proves lock is held. |
| let free_loaned_list = unsafe { self.free_loaned_list.get_mut(loaned_token) }; |
| for page in free_loaned_list.iter() { |
| asan_poison_page(page, ASAN_PMM_FREE_MAGIC); |
| } |
| } |
| |
| /// This method is racy as it allows us to read free_fill_enabled_ without holding the lock. If |
| /// we receive a value of 'true', then as there is no mechanism to re-set it to false, we |
| /// know it is still true. If we receive the value of 'false', then it could still become |
| /// 'true' later. The intent of this method is to allow for filling the free pattern outside |
| /// of the lock in most cases, and in the unlikely event of a race during the checker being |
| /// armed, the pattern can resort to being filled inside the lock. |
| fn is_free_fill_enabled_racy(&self) -> bool { |
| // Read with acquire semantics to ensure that any modifications to checker_ are visible |
| // before changes to free_fill_enabled_. See EnableFreePageFilling for where the |
| // release is performed. |
| self.free_fill_enabled.load(Ordering::Acquire) |
| } |
| |
| fn should_delay_allocation_locked(&self, token: &mut LockToken<'_, PmmNodeLockClass>) -> bool { |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if *self.should_wait.get(token) == ShouldWaitState::UntilReset { |
| return true; |
| } |
| if *self.should_wait.get(token) == ShouldWaitState::Never { |
| return false; |
| } |
| } |
| // See pmm_check_alloc_random_should_wait in pmm.rs for an assertion that random should wait |
| // is only enabled if debug_assertions. |
| if cfg!(debug_assertions) && BootOptions::get().pmm_alloc_random_should_wait { |
| // SAFETY: token proves lock is held. |
| let seed = unsafe { self.random_should_wait_seed.get_mut(token) }; |
| // SAFETY: rand_r takes a valid pointer. |
| let val = unsafe { rand_r((seed as *mut usize).cast()) }; |
| if val < (RAND_MAX / 10) { |
| return true; |
| } |
| } |
| false |
| } |
| |
| /// This method should be called when the PMM fails to allocate in a user-visible way and will |
| /// (optionally) trigger an asynchronous OOM response. |
| fn report_alloc_failure_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLockClass>, |
| failure: AllocFailure, |
| ) { |
| PMM_ALLOC_FAILED.add(1); |
| |
| // Update before signaling the MemoryWatchdog to ensure it observes the update. |
| // |
| // |alloc_failed_no_mem| latches so only need to invoke the callback once. We could call it |
| // on every failure, but that's wasteful and we don't want to spam any underlying Event (or |
| // the thread lock or the MemoryWatchdog). |
| let first_time = !self.alloc_failed_no_mem.swap(true, Ordering::Relaxed); |
| if first_time { |
| let mut first = failure; |
| // Record the free_count_ only for non-Pmm types. For PMM alloc failures, we know |
| // exactly what the free count was at the time, because we use that to determine whether |
| // the allocation should fail. |
| if failure.r#type != AllocFailureType::Pmm { |
| // SAFETY: token proves lock is held. |
| first.free_count = unsafe { self.free_count.get(token) }.load(Ordering::Relaxed); |
| } |
| // SAFETY: token proves lock is held. |
| unsafe { |
| *self.first_alloc_failure.get_mut(token) = first; |
| } |
| } |
| // SAFETY: token proves lock is held. |
| if first_time && unsafe { !self.mem_signal.get(token).is_null() } { |
| self.signal_free_memory_change_locked(token); |
| } |
| } |
| |
| fn signal_free_memory_change_locked(&self, token: &mut LockToken<'_, PmmNodeLockClass>) { |
| // SAFETY: token proves lock is held. |
| let event_ptr = unsafe { *self.mem_signal.get(token) }; |
| debug_assert!(!event_ptr.is_null()); |
| // SAFETY: event_ptr was set by caller and is non-null. |
| unsafe { |
| (*event_ptr).signal(); |
| *self.mem_signal.get_mut(token) = core::ptr::null_mut(); |
| } |
| } |
| |
| fn trip_free_pages_level_locked(&self, token: &mut LockToken<'_, PmmNodeLockClass>) { |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if *self.should_wait.get(token) == ShouldWaitState::OnceLevelTripped { |
| *self.should_wait.get_mut(token) = ShouldWaitState::UntilReset; |
| let _ = self.may_allocate_evt.unsignal(); |
| } |
| } |
| } |
| |
| fn increment_free_count_locked(&self, token: &mut LockToken<'_, PmmNodeLockClass>, count: u64) { |
| // SAFETY: token proves lock is held. |
| let new_free_count = unsafe { |
| self.free_count.get(token).fetch_add(count, Ordering::Relaxed); |
| self.free_count.get(token).load(Ordering::Relaxed) |
| }; |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if !self.mem_signal.get(token).is_null() |
| && new_free_count > *self.mem_signal_upper_bound.get(token) |
| { |
| self.signal_free_memory_change_locked(token); |
| } |
| } |
| } |
| |
| fn decrement_free_count_locked(&self, token: &mut LockToken<'_, PmmNodeLockClass>, count: u64) { |
| // SAFETY: token proves lock is held. |
| let new_free_count = unsafe { |
| debug_assert!(self.free_count.get(token).load(Ordering::Relaxed) >= count); |
| self.free_count.get(token).fetch_sub(count, Ordering::Relaxed); |
| self.free_count.get(token).load(Ordering::Relaxed) |
| }; |
| // SAFETY: token proves lock is held. |
| unsafe { |
| if *self.should_wait.get(token) == ShouldWaitState::OnceLevelTripped |
| && new_free_count < *self.should_wait_free_pages_level.get(token) |
| { |
| self.trip_free_pages_level_locked(token); |
| } |
| if !self.mem_signal.get(token).is_null() |
| && new_free_count < *self.mem_signal_lower_bound.get(token) |
| { |
| self.signal_free_memory_change_locked(token); |
| } |
| } |
| } |
| |
| fn increment_free_loaned_count_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| count: u64, |
| ) { |
| unsafe { self.free_loaned_count.get(token) }.fetch_add(count, Ordering::Relaxed); |
| } |
| |
| fn decrement_free_loaned_count_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| count: u64, |
| ) { |
| debug_assert!( |
| unsafe { self.free_loaned_count.get(token) }.load(Ordering::Relaxed) >= count |
| ); |
| unsafe { self.free_loaned_count.get(token) }.fetch_sub(count, Ordering::Relaxed); |
| } |
| |
| fn increment_loaned_count_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| count: u64, |
| ) { |
| unsafe { self.loaned_count.get(token) }.fetch_add(count, Ordering::Relaxed); |
| } |
| |
| fn decrement_loaned_count_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| count: u64, |
| ) { |
| debug_assert!(unsafe { self.loaned_count.get(token) }.load(Ordering::Relaxed) >= count); |
| unsafe { self.loaned_count.get(token) }.fetch_sub(count, Ordering::Relaxed); |
| } |
| |
| fn increment_loan_cancelled_count_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| count: u64, |
| ) { |
| unsafe { self.loan_cancelled_count.get(token) }.fetch_add(count, Ordering::Relaxed); |
| } |
| |
| fn decrement_loan_cancelled_count_locked( |
| &self, |
| token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| count: u64, |
| ) { |
| debug_assert!( |
| unsafe { self.loan_cancelled_count.get(token) }.load(Ordering::Relaxed) >= count |
| ); |
| unsafe { self.loan_cancelled_count.get(token) }.fetch_sub(count, Ordering::Relaxed); |
| } |
| |
| unsafe fn alloc_page_helper_locked(&self, page: NonNull<VmPage>) { |
| // SAFETY: page is a valid VmPage pointer. |
| unsafe { |
| let p = page.as_ref(); |
| ltracef!( |
| "allocating page {:p}, pa {:#x}, prev state {:?}\n", |
| page, |
| p.paddr().0, |
| p.state() |
| ); |
| asan_unpoison_page(p); |
| debug_assert!(p.is_free() && !p.is_loaned()); |
| // Here we transition the page from FREE->ALLOC, completing the transfer of ownership |
| // from the PmmNode to the stack. This must be done under lock, and more specifically |
| // the same lock acquisition that removes the page from the free list, as both being the |
| // free list, or being in the ALLOC state, indicate ownership by the PmmNode. |
| p.set_state(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| // Used by the FLPH for loaned pages, but cleared here for consistency to ensure no |
| // stale pointers that could be accidentally referenced. |
| (*p.state_union.get()).alloc.owner = core::ptr::null_mut(); |
| } |
| } |
| |
| unsafe fn alloc_loaned_page_helper_locked(&self, page: NonNull<VmPage>) { |
| // SAFETY: page is a valid VmPage pointer. |
| unsafe { |
| let p = page.as_ref(); |
| ltracef!( |
| "allocating loaned page {:p}, pa {:#x}, prev state {:?}\n", |
| page, |
| p.paddr().0, |
| p.state() |
| ); |
| asan_unpoison_page(p); |
| debug_assert!(p.is_free_loaned() && p.is_loaned()); |
| // Here we transition the page from FREE_LOANED->ALLOC, completing the transfer of |
| // ownership from the PmmNode to the stack. This must be done under loaned_pages_lock, |
| // and more specifically the same loaned_pages_lock acquisition that removes the page |
| // from the free list, as both being the free list, or being in the ALLOC state, |
| // indicate ownership by the PmmNode. |
| p.set_state(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| (*p.state_union.get()).alloc.owner = core::ptr::null_mut(); |
| } |
| } |
| |
| unsafe fn free_page_helper_locked( |
| &self, |
| _token: &mut LockToken<'_, PmmNodeLockClass>, |
| page: NonNull<VmPage>, |
| already_filled: bool, |
| ) { |
| // SAFETY: page is a valid VmPage pointer. |
| unsafe { |
| let p = page.as_ref(); |
| ltracef!("page {:p} state {:?} paddr {:#x}\n", page, p.state(), p.paddr().0); |
| debug_assert!(!p.is_free()); |
| debug_assert!(!p.is_free_loaned()); |
| debug_assert!( |
| p.state() != VmPageState(page_bindings::vm_page_state::OBJECT) |
| || (p.get_pin_count() == 0 && p.get_object().is_null()) |
| ); |
| // mark it free. This makes the page owned the PmmNode, even though it may not be in any |
| // page |
| // list, since the page is findable via the arena, and so we must ensure to: |
| // 1. Be performing set_state here under the lock |
| // 2. Place the page in the free list and cease referring to the page before ever |
| // dropping |
| // lock |
| p.set_state(VmPageState(page_bindings::vm_page_state::FREE)); |
| // This page cannot be loaned. |
| debug_assert!(!p.is_loaned()); |
| // The caller may have called RacyFreeFillEnabled and potentially already filled a |
| // pattern, however if it raced with enabling of free filling we may still need to fill |
| // the pattern. This should be unlikely, and since free filling can never be turned back |
| // off there is no race in the other direction. |
| if self.free_fill_enabled.load(Ordering::SeqCst) && !already_filled { |
| let vmp = VmPagePtr::new(page); |
| self.checker().fill_pattern(vmp); |
| } |
| asan_poison_page(p, ASAN_PMM_FREE_MAGIC); |
| } |
| } |
| |
| unsafe fn free_loaned_page_helper_locked( |
| &self, |
| _token: &mut LockToken<'_, PmmNodeLoanedListLockClass>, |
| page: NonNull<VmPage>, |
| already_filled: bool, |
| ) { |
| // SAFETY: page is a valid VmPage pointer. |
| unsafe { |
| let p = page.as_ref(); |
| ltracef!("page {:p} state {:?} paddr {:#x}\n", page, p.state(), p.paddr().0); |
| debug_assert!(!p.is_free()); |
| debug_assert!( |
| p.state() != VmPageState(page_bindings::vm_page_state::OBJECT) |
| || p.get_pin_count() == 0 |
| ); |
| debug_assert!( |
| p.state() != VmPageState(page_bindings::vm_page_state::ALLOC) |
| || (*p.state_union.get()).alloc.owner.is_null() |
| ); |
| // mark it free. This makes the page owned the PmmNode and even though it may not be in |
| // any page list, since the page is findable via the arena we must ensure the following |
| // happens: |
| // 1. We hold loaned_list_lock preventing pages from transition to/from loaned |
| // 2. This page is loaned and hence will not be considered by an arena traversal that |
| // holds lock |
| // 3. Perform set_state here under the loaned_list_lock |
| // 4. Place the page in the loaned_free_list and cease referring to the page before ever |
| // dropping the loaned_list_lock. |
| p.set_state(VmPageState(page_bindings::vm_page_state::FREE_LOANED)); |
| // The caller may have called IsFreeFillEnabledRacy and potentially already filled a |
| // pattern, however if it raced with enabling of free filling we may still need to fill |
| // the pattern. This should be unlikely, and since free filling can never be turned back |
| //off there is no race in the other direction. As we hold lock we can safely perform a |
| // relaxed read. |
| if !already_filled && self.free_fill_enabled.load(Ordering::SeqCst) { |
| let vmp = VmPagePtr::new(page); |
| self.checker().fill_pattern(vmp); |
| } |
| asan_poison_page(p, ASAN_PMM_FREE_MAGIC); |
| } |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_end_handoff(node: *mut PmmNode) { |
| // SAFETY: node is a valid PmmNode pointer. |
| let node = unsafe { &*node }; |
| node.end_handoff(); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_alloc_page( |
| node: *mut PmmNode, |
| alloc_flags: u32, |
| out_page: *mut *mut page_bindings::vm_page_t, |
| ) -> zx_status_t { |
| // SAFETY: node and out_page are valid pointers. |
| let node = unsafe { &*node }; |
| match node.alloc_page(alloc_flags) { |
| Ok(page) => { |
| unsafe { *out_page = page.as_ffi() }; |
| zx_types::ZX_OK |
| } |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_alloc_pages( |
| node: *mut PmmNode, |
| count: usize, |
| alloc_flags: u32, |
| list: *mut VmPageDoublyLinkedList, |
| ) -> zx_status_t { |
| // SAFETY: node and list are valid and pinned. |
| let node = unsafe { &*node }; |
| let list = unsafe { Pin::new_unchecked(&mut *list) }; |
| match node.alloc_pages(count, alloc_flags, list) { |
| Ok(()) => zx_types::ZX_OK, |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_alloc_range( |
| node: *mut PmmNode, |
| address: u64, |
| count: usize, |
| list: *mut VmPageDoublyLinkedList, |
| ) -> zx_status_t { |
| // SAFETY: node and list are valid and pinned. |
| let node = unsafe { &*node }; |
| let list = unsafe { Pin::new_unchecked(&mut *list) }; |
| match node.alloc_range(PAddr(address as usize), count, list) { |
| Ok(()) => zx_types::ZX_OK, |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_alloc_contiguous( |
| node: *mut PmmNode, |
| count: usize, |
| alloc_flags: u32, |
| alignment_log2: u8, |
| pa: *mut u64, |
| list: *mut VmPageDoublyLinkedList, |
| ) -> zx_status_t { |
| // SAFETY: node, pa, and list are valid and pinned. |
| let node = unsafe { &*node }; |
| let list = unsafe { Pin::new_unchecked(&mut *list) }; |
| match node.alloc_contiguous(count, alloc_flags, alignment_log2, list) { |
| Ok(out_pa) => { |
| unsafe { *pa = out_pa.0 as u64 }; |
| zx_types::ZX_OK |
| } |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_free_page( |
| node: *mut PmmNode, |
| page: *mut page_bindings::vm_page_t, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| // SAFETY: node and page are valid. |
| let node = unsafe { &*node }; |
| let page = unsafe { VmPagePtr::from_ffi(page) }.expect("null page"); |
| unsafe { node.free_page(page, delay_reuse) }; |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_free_list( |
| node: *mut PmmNode, |
| list: *mut VmPageDoublyLinkedList, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| // SAFETY: node and list are valid and pinned. |
| let node = unsafe { &*node }; |
| let list = unsafe { Pin::new_unchecked(&mut *list) }; |
| unsafe { node.free_list(list, delay_reuse) }; |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_with_loaned_page( |
| node: *mut PmmNode, |
| page: *mut page_bindings::vm_page_t, |
| with_page: unsafe extern "C" fn(*mut page_bindings::vm_page_t, *mut core::ffi::c_void), |
| cookie: *mut core::ffi::c_void, |
| ) { |
| // SAFETY: node and page are valid. |
| let node = unsafe { &*node }; |
| let page = unsafe { VmPagePtr::from_ffi(page) }.expect("null page"); |
| node.with_loaned_page(page, |p| { |
| // SAFETY: FFI callback with cookie. |
| unsafe { with_page(p.as_ffi(), cookie) }; |
| }); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_alloc_loaned_page( |
| node: *mut PmmNode, |
| allocated: unsafe extern "C" fn(*mut page_bindings::vm_page_t, *mut core::ffi::c_void), |
| cookie: *mut core::ffi::c_void, |
| out_page: *mut *mut page_bindings::vm_page_t, |
| ) -> zx_status_t { |
| // SAFETY: node and out_page are valid. |
| let node = unsafe { &*node }; |
| let res = node.alloc_loaned_page(|p| { |
| unsafe { allocated(p.as_ffi(), cookie) }; |
| }); |
| match res { |
| Ok(page) => { |
| unsafe { *out_page = page.as_ffi() }; |
| zx_types::ZX_OK |
| } |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_begin_free_loaned_page( |
| node: *mut PmmNode, |
| page: *mut page_bindings::vm_page_t, |
| release_page: unsafe extern "C" fn(*mut page_bindings::vm_page_t, *mut core::ffi::c_void), |
| cookie: *mut core::ffi::c_void, |
| flph: *mut FreeLoanedPagesHolder, |
| ) { |
| // SAFETY: node, page, and flph are valid. |
| let node = unsafe { &*node }; |
| let page = unsafe { VmPagePtr::from_ffi(page) }.expect("null page"); |
| let flph = unsafe { Pin::new_unchecked(&mut *flph) }; |
| unsafe { |
| node.begin_free_loaned_page( |
| page, |
| |p| { |
| release_page(p.as_ffi(), cookie); |
| }, |
| flph, |
| ); |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_finish_free_loaned_pages( |
| node: *mut PmmNode, |
| flph: *mut FreeLoanedPagesHolder, |
| ) { |
| // SAFETY: node and flph are valid. |
| let node = unsafe { &*node }; |
| let flph = unsafe { Pin::new_unchecked(&mut *flph) }; |
| node.finish_free_loaned_pages(flph); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_begin_free_loaned_array( |
| node: *mut PmmNode, |
| pages: *mut *mut page_bindings::vm_page_t, |
| count: usize, |
| release_list: unsafe extern "C" fn( |
| *mut *mut page_bindings::vm_page_t, |
| usize, |
| *mut VmPageDoublyLinkedList, |
| *mut core::ffi::c_void, |
| ), |
| cookie: *mut core::ffi::c_void, |
| flph: *mut FreeLoanedPagesHolder, |
| ) { |
| // SAFETY: node, pages, and flph are valid. |
| let node = unsafe { &*node }; |
| let raw_slice = unsafe { core::slice::from_raw_parts(pages, count) }; |
| let ptr_slice: &[VmPagePtr] = unsafe { core::mem::transmute(raw_slice) }; |
| let flph = unsafe { Pin::new_unchecked(&mut *flph) }; |
| unsafe { |
| node.begin_free_loaned_array( |
| ptr_slice, |
| |pages_slice, free_list| { |
| release_list( |
| pages, |
| pages_slice.len(), |
| (free_list.get_unchecked_mut() as *mut VmPageDoublyLinkedList).cast(), |
| cookie, |
| ); |
| }, |
| flph, |
| ); |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_unwire_page( |
| node: *mut PmmNode, |
| page: *mut page_bindings::vm_page_t, |
| ) { |
| // SAFETY: node and page are valid. |
| let node = unsafe { &*node }; |
| let page = unsafe { VmPagePtr::from_ffi(page) }.expect("null page"); |
| node.unwire_page(page); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_begin_loan( |
| node: *mut PmmNode, |
| page_list: *mut VmPageDoublyLinkedList, |
| delay_reuse: PmmOptDelayReuse, |
| ) { |
| // SAFETY: node and page_list are valid and pinned. |
| let node = unsafe { &*node }; |
| let page_list = unsafe { Pin::new_unchecked(&mut *page_list) }; |
| unsafe { node.begin_loan(page_list, delay_reuse) }; |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_cancel_loan( |
| node: *mut PmmNode, |
| page: *mut page_bindings::vm_page_t, |
| ) { |
| // SAFETY: node and page are valid. |
| let node = unsafe { &*node }; |
| let page = unsafe { VmPagePtr::from_ffi(page) }.expect("null page"); |
| unsafe { node.cancel_loan(page) }; |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_end_loan( |
| node: *mut PmmNode, |
| page: *mut page_bindings::vm_page_t, |
| ) { |
| // SAFETY: node and page are valid. |
| let node = unsafe { &*node }; |
| let page = unsafe { VmPagePtr::from_ffi(page) }.expect("null page"); |
| unsafe { node.end_loan(page) }; |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_set_free_memory_signal( |
| node: *mut PmmNode, |
| free_lower_bound: u64, |
| free_upper_bound: u64, |
| delay_allocations_pages: u64, |
| event: *mut Event, |
| ) -> bool { |
| // SAFETY: node is valid and caller guarantees event is valid. |
| unsafe { |
| let node = &*node; |
| node.set_free_memory_signal( |
| free_lower_bound, |
| free_upper_bound, |
| delay_allocations_pages, |
| event, |
| ) |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_wait_for_single_page_allocation( |
| node: *mut PmmNode, |
| deadline: zx_instant_mono_t, |
| slack_amount: zx_duration_t, |
| slack_mode: SlackMode, |
| suspendable: bool, |
| out_page: *mut *mut page_bindings::vm_page_t, |
| ) -> zx_status_t { |
| // SAFETY: node and out_page are valid. |
| let node = unsafe { &*node }; |
| let slack = |
| TimerSlack::new(crate::platform_rs::timer::DurationUnknown(slack_amount), slack_mode); |
| let deadline = Deadline::new(crate::platform_rs::timer::InstantUnknown(deadline), slack); |
| match node.wait_for_single_page_allocation(deadline, suspendable) { |
| Ok(page) => { |
| unsafe { *out_page = page.as_ffi() }; |
| zx_types::ZX_OK |
| } |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_stop_returning_should_wait(node: *mut PmmNode) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.stop_returning_should_wait(); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_count_free_pages(node: *const PmmNode) -> u64 { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.count_free_pages() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_count_loaned_free_pages(node: *const PmmNode) -> u64 { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.count_loaned_free_pages() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_count_loan_cancelled_pages(node: *const PmmNode) -> u64 { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.count_loan_cancelled_pages() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_count_loaned_not_free_pages(node: *const PmmNode) -> u64 { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.count_loaned_not_free_pages() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_count_loaned_pages(node: *const PmmNode) -> u64 { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.count_loaned_pages() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_count_total_bytes(node: *const PmmNode) -> u64 { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.count_total_bytes() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_dump_free(node: *const PmmNode) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.dump_free(); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_dump(node: *const PmmNode, is_panic: bool) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.dump(is_panic); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_get_arena_info( |
| node: *const PmmNode, |
| count: usize, |
| i: u64, |
| buffer: *mut PmmArenaInfo, |
| buffer_size: usize, |
| ) -> zx_status_t { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| let num_arenas = node.num_arenas(); |
| if count == 0 || (count + i as usize > num_arenas) || (i as usize >= num_arenas) { |
| return zx_types::ZX_ERR_OUT_OF_RANGE; |
| } |
| if buffer_size < count * core::mem::size_of::<PmmArenaInfo>() { |
| return zx_types::ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| // SAFETY: Validated count is not 0 and buffer is at least large enough. |
| let slice = |
| unsafe { core::slice::from_raw_parts_mut(buffer as *mut MaybeUninit<PmmArenaInfo>, count) }; |
| // SAFETY: buffer is checked against buffer_size in get_arena_info_raw. |
| match node.get_arena_info(i as usize, slice) { |
| Ok(result) => { |
| if result.len() < count { |
| zx_types::ZX_ERR_BUFFER_TOO_SMALL |
| } else { |
| zx_types::ZX_OK |
| } |
| } |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_set_page_compression( |
| node: *mut PmmNode, |
| compression: *mut VmCompression, |
| ) -> zx_status_t { |
| // SAFETY: node is valid. compression was passed from RefPtr::release(). |
| let node = unsafe { &*node }; |
| let ref_ptr = unsafe { fbl::RefPtr::from_raw(compression) }; |
| match node.set_page_compression(ref_ptr) { |
| Ok(()) => zx_types::ZX_OK, |
| Err(status) => status.into_raw(), |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_fill_free_pages_and_arm(node: *mut PmmNode) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.fill_free_pages_and_arm(); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_check_all_free_pages(node: *mut PmmNode) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.check_all_free_pages(); |
| } |
| |
| #[unsafe(no_mangle)] |
| #[cfg(sanitize = "address")] |
| unsafe extern "C" fn rust_pmm_node_poison_all_free_pages(node: *mut PmmNode) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.poison_all_free_pages(); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_enable_free_page_filling( |
| node: *mut PmmNode, |
| fill_size: usize, |
| action: u8, |
| ) -> bool { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| let action = match action { |
| 1 => CheckFailAction::Panic, |
| _ => CheckFailAction::Oops, |
| }; |
| node.enable_free_page_filling(fill_size, action) |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_get_alloc_failed_count() -> i64 { |
| PmmNode::get_alloc_failed_count() |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_seed_random_should_wait(node: *mut PmmNode) { |
| // SAFETY: node is valid. |
| let node = unsafe { &*node }; |
| node.seed_random_should_wait(); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_report_alloc_failure( |
| node: *mut PmmNode, |
| failure: *const AllocFailure, |
| ) { |
| // SAFETY: node and failure are valid. |
| let node = unsafe { &*node }; |
| let failure = unsafe { *failure }; |
| node.report_alloc_failure(failure); |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_get_first_alloc_failure( |
| node: *const PmmNode, |
| out_failure: *mut AllocFailure, |
| ) { |
| // SAFETY: node and out_failure are valid. |
| let node = unsafe { &*node }; |
| let failure = node.get_first_alloc_failure(); |
| unsafe { *out_failure = failure }; |
| } |
| |
| #[unsafe(no_mangle)] |
| unsafe extern "C" fn rust_pmm_node_add_free_pages( |
| node: *mut PmmNode, |
| list: *mut VmPageDoublyLinkedList, |
| ) { |
| // SAFETY: Caller guarantees these are not null and are pinned. |
| unsafe { |
| (*node).add_free_pages(Pin::new_unchecked(&mut *list)); |
| } |
| } |
| |
| /// Unit tests for PmmNode. |
| #[cfg(ktest)] |
| #[unittest::suite(name = "pmm_node_rust")] |
| mod pmm_node_rust { |
| use super::{ |
| ALLOC_FLAG_ANY, ALLOC_FLAG_CAN_WAIT, AllocFailure, AllocFailureType, FreeLoanedPagesHolder, |
| PmmNode, PmmOptDelayReuse, |
| }; |
| use crate::kernel::deadline::{Deadline, DurationMono, TimerSlack}; |
| use crate::kernel::thread; |
| use crate::platform_rs::timer::InstantMono; |
| use crate::vm::page::{VmPageDoublyLinkedList, VmPagePtr}; |
| use crate::vm::page_state::VmPageState; |
| use crate::vm::page_state::bindings::vm_page_state; |
| use crate::vm::physical_page_borrowing_config::ScopedLoaningEnabled; |
| use crate::vm::physmap::paddr_to_physmap; |
| use core::sync::atomic::{AtomicBool, AtomicI32, Ordering}; |
| use pin_init::{stack_pin_init, stack_try_pin_init}; |
| use unittest::{ |
| assert_gt, assert_ok, assert_true, expect_eq, expect_false, expect_ne, expect_ok, |
| expect_true, unwrap_ok, |
| }; |
| use zx_status::Status; |
| |
| /// Helper class for managing a PmmNode with real pages. alloc_range and alloc_contiguous are |
| /// not supported by the managed PmmNode object. Only a single instance can exist at a time. |
| #[pin_data(PinnedDrop)] |
| pub struct ManagedPmmNode { |
| #[pin] |
| node: PmmNode, |
| #[pin] |
| event: Event, |
| /// VMO that we will use to have a valid backlink for any loaned pages that get allocated. |
| vmo: fbl::RefPtr<crate::vm::vm_object_paged::VmObjectPaged>, |
| /// An optional scanner disable that is instantiated should any loaned pages get allocated. |
| /// This is needed as our backlinks, while valid pointers, will confuse reclamation if it |
| /// tries to reclaim using them. |
| scanner_disable: core::cell::RefCell<Option<crate::vm::scanner::AutoVmScannerDisable>>, |
| } |
| |
| impl ManagedPmmNode { |
| pub const NUM_PAGES: usize = 64; |
| pub const DEFAULT_MEM_EVENT_LOWER_BOUND: u64 = (Self::NUM_PAGES / 2) as u64; |
| pub const DEFAULT_SHOULD_WAIT_LEVEL: u64 = (Self::NUM_PAGES / 4) as u64; |
| |
| pub const DEFAULT_LOW_MEM_ALLOC: usize = |
| Self::NUM_PAGES - Self::DEFAULT_SHOULD_WAIT_LEVEL as usize + 1; |
| pub const DEFAULT_MEM_EVENT_ALLOC: usize = |
| Self::NUM_PAGES - Self::DEFAULT_MEM_EVENT_LOWER_BOUND as usize + 1; |
| |
| pub fn init() -> impl PinInit<Self, Status> { |
| pin_init!(&_this in Self { |
| node <- PmmNode::init(), |
| event <- Event::init_unsignaled(), |
| vmo: crate::vm::vm_object_paged::VmObjectPaged::create(0, 0, 0)?, |
| scanner_disable: core::cell::RefCell::new(None), |
| }? Status) |
| } |
| |
| pub fn setup(self: Pin<&mut Self>) -> Result<(), Status> { |
| pin_init::stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| crate::vm::pmm::alloc_pages(Self::NUM_PAGES, 0, list.as_mut())?; |
| for page in list.iter() { |
| // TODO: Prevent this page state from allowing AllocContiguous() to potentially find |
| // run of FREE pages involving some of these pages. |
| // SAFETY: Setting page state for initialized test pages. |
| unsafe { |
| page.set_state(VmPageState(vm_page_state::FREE)); |
| } |
| } |
| // SAFETY: Destructuring pinned ManagedPmmNode during setup. |
| let this = unsafe { self.get_unchecked_mut() }; |
| // SAFETY: Pages were allocated and transitioned to FREE state above. |
| unsafe { this.node.add_free_pages(list.as_mut()) }; |
| |
| assert!(this.node.enable_free_page_filling( |
| page::SIZE, |
| crate::vm::pmm_checker::CheckFailAction::Panic |
| )); |
| this.node.fill_free_pages_and_arm(); |
| |
| let result = this.reset_default_mem_event(); |
| assert!(result); |
| |
| Ok(()) |
| } |
| |
| pub fn is_event_signaled(&self) -> bool { |
| self.event.wait(&crate::kernel::deadline::Deadline::infinite_past()).is_ok() |
| } |
| |
| pub fn unsignal_event(&self) { |
| let _ = self.event.unsignal(); |
| } |
| |
| pub fn reset_default_mem_event(&self) -> bool { |
| self.set_free_memory_signal( |
| Self::DEFAULT_MEM_EVENT_LOWER_BOUND, |
| u64::MAX, |
| Self::DEFAULT_SHOULD_WAIT_LEVEL, |
| ) |
| } |
| |
| pub fn set_free_memory_signal( |
| &self, |
| lower_bound: u64, |
| higher_bound: u64, |
| delay_pages: u64, |
| ) -> bool { |
| // SAFETY: self.event is pinned and valid. |
| unsafe { |
| self.node.set_free_memory_signal( |
| lower_bound, |
| higher_bound, |
| delay_pages, |
| &self.event as *const Event as *mut Event, |
| ) |
| } |
| } |
| |
| pub fn node(&self) -> &PmmNode { |
| &self.node |
| } |
| |
| pub fn alloc_loaned_pages( |
| &self, |
| count: usize, |
| pages: &mut [Option<VmPagePtr>], |
| ) -> Result<(), Status> { |
| let mut scanner_disable = self.scanner_disable.borrow_mut(); |
| if scanner_disable.is_none() { |
| *scanner_disable = Some(crate::vm::scanner::AutoVmScannerDisable::new()); |
| } |
| let cow = self.vmo.debug_get_cow_pages().ok_or(Status::INTERNAL)?; |
| for i in 0..count { |
| let result = self.node.alloc_loaned_page(|page| { |
| // SAFETY: Initializing loaned page backlink and state for test. |
| unsafe { |
| page.set_state(VmPageState(vm_page_state::OBJECT)); |
| page.as_ref().set_object(core::ptr::null_mut()); |
| page.as_ref().set_page_offset(0); |
| crate::vm::pmm::node().page_queues().set_reclaim(page, &cow, 0); |
| } |
| }); |
| match result { |
| Ok(page) => { |
| pages[i] = Some(page); |
| } |
| Err(status) => { |
| for p in pages.iter().take(i).flatten() { |
| self.free_loaned_page(*p); |
| } |
| return Err(status); |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| pub fn free_loaned_page(&self, page: VmPagePtr) { |
| pin_init::stack_pin_init!(let flph = FreeLoanedPagesHolder::init()); |
| // SAFETY: page was allocated as loaned and flph is a valid pinned holder. |
| unsafe { |
| self.node.begin_free_loaned_page( |
| page, |
| |p| crate::vm::pmm::node().page_queues().remove(p), |
| flph.as_mut(), |
| ); |
| } |
| self.node.finish_free_loaned_pages(flph.as_mut()); |
| } |
| } |
| |
| #[pin_init::pinned_drop] |
| impl PinnedDrop for ManagedPmmNode { |
| fn drop(self: core::pin::Pin<&mut Self>) { |
| // SAFETY: Destructuring pinned ManagedPmmNode during drop. |
| let this = unsafe { self.get_unchecked_mut() }; |
| pin_init::stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = this.node.alloc_pages(Self::NUM_PAGES, 0, list.as_mut()); |
| assert_eq!(status, Ok(())); |
| for page in list.iter() { |
| // SAFETY: Resetting page state to ALLOC so they can be freed to pmm. |
| unsafe { |
| page.set_state(VmPageState(vm_page_state::ALLOC)); |
| } |
| } |
| // SAFETY: list contains valid allocated pages to return to pmm. |
| unsafe { crate::vm::pmm::free_list(list) }; |
| } |
| } |
| |
| /// Tests simple creation and destruction. |
| #[test] |
| fn smoke() { |
| stack_pin_init!(let _pmm = PmmNode::init()); |
| } |
| |
| /// Allocates more than one page and frees them. |
| #[test] |
| fn node_multi_alloc() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| let alloc_count = ManagedPmmNode::NUM_PAGES / 2; |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| |
| let status = node.node().alloc_pages(alloc_count, 0, list.as_mut()); |
| expect_ok!(status, "pmm_alloc_pages a few pages"); |
| expect_eq!(alloc_count, list.iter().count(), "pmm_alloc_pages a few pages list count"); |
| |
| let status = node.node().alloc_pages(alloc_count, 0, list.as_mut()); |
| expect_ok!(status, "pmm_alloc_pages a few pages"); |
| expect_eq!(2 * alloc_count, list.iter().count(), "pmm_alloc_pages a few pages list count"); |
| |
| // SAFETY: list contains pages allocated from node. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Allocates one page from the bulk allocation api. |
| #[test] |
| fn node_singleton_list() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| |
| let status = node.node().alloc_pages(1, 0, list.as_mut()); |
| expect_ok!(status, "pmm_alloc_pages a few pages"); |
| expect_eq!(1, list.iter().count(), "pmm_alloc_pages a few pages list count"); |
| |
| // SAFETY: list contains pages allocated from node. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Loans pages, borrows, cancels, reclaims, and ends the loan. |
| #[test] |
| fn node_loan_borrow_cancel_reclaim_end() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| let _cleanup = ScopedLoaningEnabled::new(true); |
| |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| |
| const LOAN_COUNT: usize = ManagedPmmNode::NUM_PAGES * 3 / 4; |
| const NOT_LOAN_COUNT: usize = ManagedPmmNode::NUM_PAGES - LOAN_COUNT; |
| let mut paddr = [crate::kernel::types::PAddr(0); LOAN_COUNT]; |
| |
| let status = node.node().alloc_pages(LOAN_COUNT, 0, list.as_mut()); |
| expect_ok!(status, "pmm_alloc_pages a few pages"); |
| expect_eq!(LOAN_COUNT, list.iter().count(), "pmm_alloc_pages correct # pages"); |
| |
| for (i, page) in list.iter().enumerate() { |
| paddr[i] = page.paddr(); |
| } |
| |
| for page in list.iter() { |
| expect_false!(page.is_loaned()); |
| expect_false!(page.is_loan_cancelled()); |
| } |
| // SAFETY: list contains valid allocated pages to loan. |
| unsafe { |
| node.node().begin_loan(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| for page in list.iter() { |
| expect_true!(page.is_loaned()); |
| expect_false!(page.is_loan_cancelled()); |
| } |
| |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loaned_pages()); |
| expect_eq!(NOT_LOAN_COUNT as u64, node.node().count_free_pages()); |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loaned_free_pages()); |
| expect_eq!(0, node.node().count_loan_cancelled_pages()); |
| expect_eq!(0, node.node().count_loaned_not_free_pages()); |
| |
| expect_eq!(0, list.iter().count()); |
| let mut loaned_pages = [None; LOAN_COUNT]; |
| let status = node.alloc_loaned_pages(LOAN_COUNT, &mut loaned_pages); |
| expect_ok!(status, "pmm_alloc_pages PMM_ALLOC_FLAG_LOANED"); |
| |
| for p in loaned_pages.iter() { |
| let p = p.unwrap(); |
| let mut i = 0; |
| while i < LOAN_COUNT { |
| // SAFETY: p is a valid loaned page pointer. |
| if paddr[i] == unsafe { p.paddr() } { |
| break; |
| } |
| i += 1; |
| } |
| expect_ne!(LOAN_COUNT, i); |
| } |
| |
| for p in loaned_pages.iter() { |
| let p = p.unwrap(); |
| // SAFETY: p is a valid loaned page pointer. |
| expect_true!(unsafe { p.is_loaned() }); |
| // SAFETY: p is a valid loaned page pointer. |
| expect_false!(unsafe { p.is_loan_cancelled() }); |
| // SAFETY: p is a valid loaned page pointer. |
| unsafe { |
| node.node().cancel_loan(p); |
| } |
| // SAFETY: p is a valid loaned page pointer. |
| expect_true!(unsafe { p.is_loaned() }); |
| // SAFETY: p is a valid loaned page pointer. |
| expect_true!(unsafe { p.is_loan_cancelled() }); |
| } |
| |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loaned_pages()); |
| expect_eq!(NOT_LOAN_COUNT as u64, node.node().count_free_pages()); |
| expect_eq!(0, node.node().count_loaned_free_pages()); |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loan_cancelled_pages()); |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loaned_not_free_pages()); |
| |
| for p in loaned_pages.iter() { |
| node.free_loaned_page(p.unwrap()); |
| } |
| |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loaned_pages()); |
| expect_eq!(NOT_LOAN_COUNT as u64, node.node().count_free_pages()); |
| expect_eq!(0, node.node().count_loaned_free_pages()); |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loan_cancelled_pages()); |
| expect_eq!(LOAN_COUNT as u64, node.node().count_loaned_not_free_pages()); |
| |
| expect_eq!(0, list.iter().count()); |
| let mut extra_loaned = [None; NOT_LOAN_COUNT + 1]; |
| let status = node.alloc_loaned_pages(NOT_LOAN_COUNT + 1, &mut extra_loaned); |
| expect_true!(status == Err(Status::NO_RESOURCES), "try to allocate a loan_cancelled page"); |
| |
| expect_eq!(0, list.iter().count()); |
| let status = node.node().alloc_pages(NOT_LOAN_COUNT, ALLOC_FLAG_ANY, list.as_mut()); |
| expect_ok!(status, "allocate all the not-loaned pages"); |
| |
| for page in list.iter() { |
| let paddr_page = page.paddr(); |
| expect_false!(page.is_loaned()); |
| let mut i = 0; |
| while i < LOAN_COUNT { |
| if paddr[i] == paddr_page { |
| break; |
| } |
| i += 1; |
| } |
| expect_eq!(LOAN_COUNT, i); |
| } |
| |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| |
| expect_eq!(0, list.iter().count()); |
| for j in 0..LOAN_COUNT { |
| let page = loaned_pages[j].unwrap(); |
| // SAFETY: page is a valid loaned page pointer. |
| expect_eq!(paddr[j].0, unsafe { page.paddr() }.0); |
| // SAFETY: page is a valid loaned page pointer. |
| unsafe { |
| node.node().end_loan(page); |
| } |
| // SAFETY: page is a valid page pointer. |
| expect_false!(unsafe { page.is_loaned() }); |
| // SAFETY: page is a valid page pointer. |
| expect_false!(unsafe { page.is_loan_cancelled() }); |
| // SAFETY: list is pinned on stack, push_back_raw does not move list. |
| unsafe { list.as_mut().get_unchecked_mut().push_back_raw(page.as_non_null()) }; |
| } |
| |
| // SAFETY: list contains unloaned allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| |
| expect_eq!(0, node.node().count_loaned_pages()); |
| expect_eq!(ManagedPmmNode::NUM_PAGES as u64, node.node().count_free_pages()); |
| expect_eq!(0, node.node().count_loaned_free_pages()); |
| expect_eq!(0, node.node().count_loan_cancelled_pages()); |
| expect_eq!(0, node.node().count_loaned_not_free_pages()); |
| |
| expect_eq!(0, list.iter().count()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status, "allocate all pages"); |
| expect_eq!(ManagedPmmNode::NUM_PAGES, list.iter().count()); |
| |
| for page in list.iter() { |
| expect_false!(page.is_loaned()); |
| expect_false!(page.is_loan_cancelled()); |
| } |
| |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| |
| expect_eq!(0, node.node().count_loaned_pages()); |
| expect_eq!(ManagedPmmNode::NUM_PAGES as u64, node.node().count_free_pages()); |
| expect_eq!(0, node.node().count_loaned_free_pages()); |
| expect_eq!(0, node.node().count_loan_cancelled_pages()); |
| expect_eq!(0, node.node().count_loaned_not_free_pages()); |
| } |
| |
| /// Allocates too many pages and makes sure it fails nicely. |
| #[test] |
| fn node_oversized_alloc() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES + 1, 0, list.as_mut()); |
| expect_true!(status == Err(Status::NO_MEMORY), "pmm_alloc_pages failed to alloc"); |
| expect_true!(list.is_empty(), "pmm_alloc_pages list is empty"); |
| } |
| |
| /// Check that free memory events work correctly. |
| #[test] |
| fn node_free_mem_event() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| let free_count = node.node().count_free_pages(); |
| assert_gt!(free_count, 0); |
| |
| // Setting an event range that does not include the current free count should be invalid. |
| expect_false!(node.set_free_memory_signal(free_count + 1, u64::MAX, 0)); |
| expect_false!(node.set_free_memory_signal(0, free_count - 1, 0)); |
| |
| // The range can be inclusive of the current free count. |
| expect_true!(node.set_free_memory_signal(free_count, u64::MAX, 0)); |
| expect_true!(node.set_free_memory_signal(0, free_count, 0)); |
| |
| // Reset back to the default event. |
| expect_true!(node.reset_default_mem_event()); |
| |
| // Should never have triggered the event up to this point. |
| expect_false!(node.is_event_signaled()); |
| |
| // Allocate all but 1 of the pages to trigger the event. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| |
| for _i in 1..ManagedPmmNode::DEFAULT_MEM_EVENT_ALLOC { |
| let page = unwrap_ok!(node.node().alloc_page(0)); |
| // SAFETY: mutating pinned list without moving it. |
| unsafe { list.as_mut().get_unchecked_mut().push_back_raw(page.as_non_null()) }; |
| } |
| // Should not have triggered the event yet. |
| expect_false!(node.is_event_signaled()); |
| |
| // Allocate the last page, this should put us over the limit and set the event. |
| { |
| let page = unwrap_ok!(node.node().alloc_page(0)); |
| // SAFETY: mutating pinned list without moving it. |
| unsafe { list.as_mut().get_unchecked_mut().push_back_raw(page.as_non_null()) }; |
| } |
| expect_true!(node.is_event_signaled()); |
| node.unsignal_event(); |
| |
| // Events are one-shot, and so putting a page back and allocating it again should not |
| // re-trigger the event. |
| // SAFETY: popping from pinned list without moving list. |
| let pop_page = unsafe { list.as_mut().get_unchecked_mut().pop_front().unwrap() }; |
| // SAFETY: pop_page is a valid pointer. |
| unsafe { |
| node.node().free_page(VmPagePtr::new(pop_page), PmmOptDelayReuse::Default); |
| } |
| { |
| let page = unwrap_ok!(node.node().alloc_page(0)); |
| // SAFETY: mutating pinned list without moving it. |
| unsafe { list.as_mut().get_unchecked_mut().push_back_raw(page.as_non_null()) }; |
| } |
| expect_false!(node.is_event_signaled()); |
| |
| // Set a new free range that should trip as we return the pages back. |
| expect_true!(node.set_free_memory_signal(0, (ManagedPmmNode::NUM_PAGES - 1) as u64, 0)); |
| |
| // Take one page off the list as our final page. |
| // SAFETY: popping from pinned list without moving list. |
| let page_raw = unsafe { list.as_mut().get_unchecked_mut().pop_front().unwrap() }; |
| let page = VmPagePtr::new(page_raw); |
| |
| // Return the rest of the list. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| // Event should not have tripped yet. |
| expect_false!(node.is_event_signaled()); |
| |
| // Return the last page, should trip. |
| // SAFETY: page was allocated and is owned by this test. |
| unsafe { |
| node.node().free_page(page, PmmOptDelayReuse::Default); |
| } |
| expect_true!(node.is_event_signaled()); |
| } |
| |
| /// Checks sync allocation failure when the node crosses a threshold. |
| #[test] |
| fn node_low_mem_alloc_failure() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| |
| // Put the node in an oom state and make sure allocation fails. |
| let status = |
| node.node().alloc_pages(ManagedPmmNode::DEFAULT_LOW_MEM_ALLOC, 0, list.as_mut()); |
| expect_ok!(status); |
| // Should also have been signaled. |
| expect_true!(node.is_event_signaled()); |
| |
| let result = node.node().alloc_page(ALLOC_FLAG_CAN_WAIT); |
| expect_true!(result == Err(Status::SHOULD_WAIT)); |
| |
| // Waiting for an allocation should block. |
| expect_true!( |
| node.node().wait_for_single_page_allocation( |
| Deadline::after_mono(DurationMono::from_millis(10), TimerSlack::none()), |
| true |
| ) == Err(Status::TIMED_OUT) |
| ); |
| |
| // Free the list. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| |
| // Allocations will still be delayed until we reset the trigger. |
| let result = node.node().alloc_page(ALLOC_FLAG_CAN_WAIT); |
| expect_true!(result == Err(Status::SHOULD_WAIT)); |
| |
| expect_true!(node.reset_default_mem_event()); |
| |
| // Allocations should work again. |
| { |
| let alloc_page = |
| node.node().wait_for_single_page_allocation(Deadline::infinite_past(), true); |
| assert_true!(alloc_page != Err(Status::TIMED_OUT)); |
| if let Ok(page) = alloc_page { |
| // SAFETY: page was allocated and is owned by this test. |
| unsafe { |
| node.node().free_page(page, PmmOptDelayReuse::Default); |
| } |
| } |
| } |
| |
| // Reset the signal. |
| node.unsignal_event(); |
| // Set a threshold such that a single allocation should trip into the low mem state. |
| expect_true!(node.set_free_memory_signal( |
| ManagedPmmNode::NUM_PAGES as u64, |
| u64::MAX, |
| ManagedPmmNode::NUM_PAGES as u64 |
| )); |
| |
| // Signal should not yet be set, and allocations should not be delayed. |
| expect_false!(node.is_event_signaled()); |
| { |
| let alloc_page = |
| node.node().wait_for_single_page_allocation(Deadline::infinite_past(), true); |
| assert_true!(alloc_page != Err(Status::TIMED_OUT)); |
| if let Ok(page) = alloc_page { |
| // SAFETY: page was allocated and is owned by this test. |
| unsafe { |
| node.node().free_page(page, PmmOptDelayReuse::Default); |
| } |
| } |
| } |
| |
| // Allocate a single page and validate that allocations are now delayed. |
| assert_ok!(node.node().alloc_pages(1, 0, list.as_mut())); |
| let result = node.node().alloc_page(ALLOC_FLAG_CAN_WAIT); |
| expect_true!(result == Err(Status::SHOULD_WAIT)); |
| expect_true!( |
| node.node().wait_for_single_page_allocation( |
| Deadline::after_mono(DurationMono::from_millis(10), TimerSlack::none()), |
| true |
| ) == Err(Status::TIMED_OUT) |
| ); |
| |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Test reporting allocation failures and latching the first failure. |
| #[test] |
| fn node_alloc_failure_reporting() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Initially, no allocation failure should be recorded. |
| expect_false!(node.node().has_alloc_failed_no_mem()); |
| let initial_failure = node.node().get_first_alloc_failure(); |
| expect_true!(initial_failure.r#type == AllocFailureType::None); |
| expect_eq!(0, initial_failure.size); |
| |
| // Report a first allocation failure. |
| let failure1 = AllocFailure { r#type: AllocFailureType::Heap, size: 1024, free_count: 10 }; |
| node.node().report_alloc_failure(failure1); |
| |
| expect_true!(node.node().has_alloc_failed_no_mem()); |
| let recorded_failure = node.node().get_first_alloc_failure(); |
| expect_true!(recorded_failure.r#type == AllocFailureType::Heap); |
| expect_eq!(1024, recorded_failure.size); |
| expect_eq!(ManagedPmmNode::NUM_PAGES as u64, recorded_failure.free_count); |
| |
| // Report a second allocation failure with different parameters. |
| let failure2 = AllocFailure { r#type: AllocFailureType::Pmm, size: 4096, free_count: 5 }; |
| node.node().report_alloc_failure(failure2); |
| |
| // The node should still retain the first recorded failure. |
| let latched_failure = node.node().get_first_alloc_failure(); |
| expect_true!(latched_failure.r#type == AllocFailureType::Heap); |
| expect_eq!(1024, latched_failure.size); |
| expect_eq!(ManagedPmmNode::NUM_PAGES as u64, latched_failure.free_count); |
| } |
| |
| /// Test that deliberately putting into a no alloc state (and back out) works. |
| #[test] |
| fn node_explicit_should_wait() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| // Allocations that can wait should be blocked. |
| let result = node.node().alloc_page(ALLOC_FLAG_CAN_WAIT); |
| expect_true!(result == Err(Status::SHOULD_WAIT)); |
| expect_true!( |
| node.node().wait_for_single_page_allocation( |
| Deadline::after_mono(DurationMono::from_millis(10), TimerSlack::none()), |
| true |
| ) == Err(Status::TIMED_OUT) |
| ); |
| |
| // A regular allocation should work. |
| let result = unwrap_ok!(node.node().alloc_page(0)); |
| // SAFETY: result is a valid allocated page. |
| unsafe { |
| node.node().free_page(result, PmmOptDelayReuse::Default); |
| } |
| |
| // Changing the delayed threshold should re-enable allocations. |
| expect_true!(node.reset_default_mem_event()); |
| |
| { |
| let alloc_page = |
| node.node().wait_for_single_page_allocation(Deadline::infinite_past(), true); |
| assert_true!(alloc_page != Err(Status::TIMED_OUT)); |
| if let Ok(page) = alloc_page { |
| // SAFETY: page was allocated and is owned by this test. |
| unsafe { |
| node.node().free_page(page, PmmOptDelayReuse::Default); |
| } |
| } |
| } |
| } |
| |
| struct PmmWaiterArgs { |
| node: *const PmmNode, |
| timeout_count: *const AtomicI32, |
| no_memory_count: *const AtomicI32, |
| } |
| |
| // SAFETY: Raw pointers point to valid test data living for the test duration. |
| unsafe impl Send for PmmWaiterArgs {} |
| // SAFETY: Raw pointers point to valid test data living for the test duration. |
| unsafe impl Sync for PmmWaiterArgs {} |
| |
| extern "C" fn pmm_waiter_thread(arg: *mut core::ffi::c_void) -> i32 { |
| // SAFETY: arg is a valid pointer to PmmWaiterArgs passed during thread spawn. |
| let args = unsafe { &*(arg as *const PmmWaiterArgs) }; |
| // SAFETY: PmmWaiterArgs fields point to valid objects living for the test duration. |
| let node = unsafe { &*args.node }; |
| // SAFETY: PmmWaiterArgs fields point to valid objects living for the test duration. |
| let timeout_count = unsafe { &*args.timeout_count }; |
| // SAFETY: PmmWaiterArgs fields point to valid objects living for the test duration. |
| let no_memory_count = unsafe { &*args.no_memory_count }; |
| |
| let result = node.wait_for_single_page_allocation( |
| Deadline::after_mono(DurationMono::from_seconds(2), TimerSlack::none()), |
| true, |
| ); |
| match result { |
| Err(Status::TIMED_OUT) => { |
| timeout_count.fetch_add(1, Ordering::Relaxed); |
| } |
| Err(Status::NO_MEMORY) => { |
| no_memory_count.fetch_add(1, Ordering::Relaxed); |
| } |
| // SAFETY: page was allocated by wait_for_single_page_allocation. |
| Ok(page) => unsafe { |
| node.free_page(page, PmmOptDelayReuse::Default); |
| }, |
| _ => {} |
| } |
| 0 |
| } |
| |
| /// Verifies that WaitForSinglePageAllocation does not block after StopReturningShouldWait. |
| #[test] |
| fn node_stop_returning_should_wait() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Allocate all pages to ensure AllocPage fails with NO_MEMORY later. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| let timeout_count = AtomicI32::new(0); |
| let no_memory_count = AtomicI32::new(0); |
| let args = PmmWaiterArgs { |
| node: node.node() as *const _, |
| timeout_count: &timeout_count as *const _, |
| no_memory_count: &no_memory_count as *const _, |
| }; |
| |
| // Start a thread that will wait. |
| // SAFETY: args outlives the spawned thread which is joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"pmm waiter".as_ptr(), |
| pmm_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| |
| // Give the thread time to block. |
| let _ = thread::sleep_relative(DurationMono::from_millis(100)); |
| |
| // Stop returning should wait. This should wake up the thread. |
| node.node().stop_returning_should_wait(); |
| |
| // Wait for the thread to complete. |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.join(InstantMono::INFINITE) }; |
| |
| // Verify that the thread did not time out. |
| expect_eq!(timeout_count.load(Ordering::Relaxed), 0); |
| // Verify that the thread failed with NO_MEMORY. |
| expect_eq!(no_memory_count.load(Ordering::Relaxed), 1); |
| |
| // Second call: may_allocate_evt_ might be unsignaled but since should_wait_ is Never, it |
| // should not wait. |
| let alloc_page2 = node.node().wait_for_single_page_allocation( |
| Deadline::after_mono(DurationMono::from_millis(10), TimerSlack::none()), |
| true, |
| ); |
| expect_true!(alloc_page2 == Err(Status::NO_MEMORY)); |
| |
| // Clean up. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Verifies that all threads blocked on WaitForSinglePageAllocation are woken up. |
| #[test] |
| fn node_stop_returning_should_wait_concurrent() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Allocate all pages to ensure AllocPage fails with NO_MEMORY later. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| let timeout_count = AtomicI32::new(0); |
| let no_memory_count = AtomicI32::new(0); |
| let args = PmmWaiterArgs { |
| node: node.node() as *const _, |
| timeout_count: &timeout_count as *const _, |
| no_memory_count: &no_memory_count as *const _, |
| }; |
| |
| const NUM_WAITERS: usize = 3; |
| let mut threads = [None; NUM_WAITERS]; |
| |
| for t in threads.iter_mut() { |
| // SAFETY: args outlives the spawned threads which are joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"pmm waiter".as_ptr(), |
| pmm_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| *t = Some(thread); |
| } |
| |
| // Give threads time to block. |
| let _ = thread::sleep_relative(DurationMono::from_millis(100)); |
| |
| // Stop returning should wait. |
| node.node().stop_returning_should_wait(); |
| |
| // Wait for all threads to complete. |
| for t in threads.iter() { |
| // SAFETY: t contains a valid Thread handle. |
| let _ = unsafe { t.unwrap().join(InstantMono::INFINITE) }; |
| } |
| |
| // Verify that NO threads timed out. |
| expect_eq!(timeout_count.load(Ordering::Relaxed), 0); |
| // Verify that all threads failed with NO_MEMORY. |
| expect_eq!(no_memory_count.load(Ordering::Relaxed), NUM_WAITERS as i32); |
| |
| // Clean up. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| struct PmmSuspendKillWaiterArgs { |
| node: *const PmmNode, |
| suspendable: bool, |
| timeout: DurationMono, |
| result: *const AtomicI32, |
| } |
| |
| // SAFETY: Raw pointers point to valid test data living for the test duration. |
| unsafe impl Send for PmmSuspendKillWaiterArgs {} |
| // SAFETY: Raw pointers point to valid test data living for the test duration. |
| unsafe impl Sync for PmmSuspendKillWaiterArgs {} |
| |
| extern "C" fn pmm_suspend_kill_waiter_thread(arg: *mut core::ffi::c_void) -> i32 { |
| // SAFETY: arg is a valid pointer to PmmSuspendKillWaiterArgs passed during thread spawn. |
| let args = unsafe { &*(arg as *const PmmSuspendKillWaiterArgs) }; |
| // SAFETY: PmmSuspendKillWaiterArgs fields point to valid objects living for the test |
| // duration. |
| let node = unsafe { &*args.node }; |
| // SAFETY: PmmSuspendKillWaiterArgs fields point to valid objects living for the test |
| // duration. |
| let result = unsafe { &*args.result }; |
| |
| let res = node.wait_for_single_page_allocation( |
| Deadline::after_mono(args.timeout, TimerSlack::none()), |
| args.suspendable, |
| ); |
| // SAFETY: page was allocated by wait_for_single_page_allocation. |
| let res = res.map(|page| unsafe { |
| node.free_page(page, PmmOptDelayReuse::Default); |
| }); |
| let status = Status::result_into_raw(res); |
| result.store(status, Ordering::Relaxed); |
| 0 |
| } |
| |
| /// Verifies that suspendable WaitForSinglePageAllocation is interrupted by suspension. |
| #[test] |
| fn node_suspendable_wait() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Allocate all pages to ensure AllocPage fails with NO_MEMORY later. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| let result = AtomicI32::new(zx_types::ZX_OK); |
| let args = PmmSuspendKillWaiterArgs { |
| node: node.node() as *const _, |
| suspendable: true, |
| timeout: DurationMono::from_seconds(5), |
| result: &result as *const _, |
| }; |
| |
| // Start a thread that will wait in a suspendable state. |
| // SAFETY: args outlives the spawned thread which is joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"pmm suspendable waiter".as_ptr(), |
| pmm_suspend_kill_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| |
| // Give the thread time to block. |
| let _ = thread::sleep_relative(DurationMono::from_millis(100)); |
| |
| // Suspend the thread. |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.suspend() }; |
| |
| // Wait for the thread to complete (it should exit immediately due to suspension). |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.join(InstantMono::INFINITE) }; |
| |
| // Verify that the thread returned ZX_ERR_INTERNAL_INTR_RETRY. |
| expect_eq!(result.load(Ordering::Relaxed), Status::INTERRUPTED_RETRY.into_raw()); |
| |
| // Clean up. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Verifies that non-suspendable WaitForSinglePageAllocation ignores suspend signals. |
| #[test] |
| fn node_non_suspendable_wait() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Allocate all pages to ensure AllocPage fails with NO_MEMORY later. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| // Use a short 200ms timeout so the test completes quickly. |
| let result = AtomicI32::new(zx_types::ZX_OK); |
| let args = PmmSuspendKillWaiterArgs { |
| node: node.node() as *const _, |
| suspendable: false, |
| timeout: DurationMono::from_millis(200), |
| result: &result as *const _, |
| }; |
| |
| // Start a thread that will wait in a non-suspendable state. |
| // SAFETY: args outlives the spawned thread which is joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"pmm non-suspendable waiter".as_ptr(), |
| pmm_suspend_kill_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| |
| // Give the thread time to block. |
| let _ = thread::sleep_relative(DurationMono::from_millis(50)); |
| |
| // Suspend the thread (which should be ignored by the Wait loop). |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.suspend() }; |
| |
| // Wait for the thread to complete (it should wait out the full 200ms timeout). |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.join(InstantMono::INFINITE) }; |
| |
| // Verify that the thread returned ZX_ERR_TIMED_OUT instead of ZX_ERR_INTERNAL_INTR_RETRY. |
| expect_eq!(result.load(Ordering::Relaxed), Status::TIMED_OUT.into_raw()); |
| |
| // Clean up. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Verifies that WaitForSinglePageAllocation is interrupted when the thread is killed. |
| #[test] |
| fn node_killed_wait() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Allocate all pages to ensure AllocPage fails with NO_MEMORY later. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| // Use a long timeout so the test doesn't time out. |
| let result = AtomicI32::new(zx_types::ZX_OK); |
| let args = PmmSuspendKillWaiterArgs { |
| node: node.node() as *const _, |
| suspendable: true, |
| timeout: DurationMono::from_seconds(5), |
| result: &result as *const _, |
| }; |
| |
| // Start a thread that will wait. |
| // SAFETY: args outlives the spawned thread which is joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"pmm killed waiter".as_ptr(), |
| pmm_suspend_kill_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| |
| // Give the thread time to block. |
| let _ = thread::sleep_relative(DurationMono::from_millis(100)); |
| |
| // Kill the thread. |
| // SAFETY: thread is a valid Thread handle. |
| unsafe { thread.kill() }; |
| |
| // Wait for the thread to complete. |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.join(InstantMono::INFINITE) }; |
| |
| // Verify that the thread returned ZX_ERR_INTERNAL_INTR_KILLED. |
| expect_eq!(result.load(Ordering::Relaxed), zx_types::ZX_ERR_INTERNAL_INTR_KILLED); |
| |
| // Clean up. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Verifies that non-suspendable WaitForSinglePageAllocation is interrupted when killed. |
| #[test] |
| fn node_suspend_then_killed_wait() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| // Allocate all pages to ensure AllocPage fails with NO_MEMORY later. |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| let status = node.node().alloc_pages(ManagedPmmNode::NUM_PAGES, 0, list.as_mut()); |
| expect_ok!(status); |
| |
| // Place the node directly into a state that forbids allocations. |
| expect_true!(node.set_free_memory_signal(0, ManagedPmmNode::NUM_PAGES as u64, u64::MAX)); |
| |
| // Use a long timeout so the test doesn't time out naturally. |
| let result = AtomicI32::new(zx_types::ZX_OK); |
| let args = PmmSuspendKillWaiterArgs { |
| node: node.node() as *const _, |
| suspendable: false, |
| timeout: DurationMono::from_seconds(5), |
| result: &result as *const _, |
| }; |
| |
| // Start a thread that will wait in a non-suspendable state. |
| // SAFETY: args outlives the spawned thread which is joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"pmm suspend-then-killed waiter".as_ptr(), |
| pmm_suspend_kill_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| |
| // Give the thread time to block. |
| let _ = thread::sleep_relative(DurationMono::from_millis(100)); |
| |
| // Suspend the thread (which should be ignored). |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.suspend() }; |
| |
| // Give it some time to ensure it's still blocked. |
| let _ = thread::sleep_relative(DurationMono::from_millis(50)); |
| |
| // Now kill the thread. |
| // SAFETY: thread is a valid Thread handle. |
| unsafe { thread.kill() }; |
| |
| // Wait for the thread to complete. |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.join(InstantMono::INFINITE) }; |
| |
| // Verify that the thread returned ZX_ERR_INTERNAL_INTR_KILLED. |
| expect_eq!(result.load(Ordering::Relaxed), zx_types::ZX_ERR_INTERNAL_INTR_KILLED); |
| |
| // Clean up. |
| // SAFETY: list contains allocated pages. |
| unsafe { |
| node.node().free_list(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| /// Verifies that AllocPages appends to an existing list without re-running the checker. |
| #[test] |
| fn alloc_append() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| stack_pin_init!(let alloc_list = VmPageDoublyLinkedList::new()); |
| |
| // Allocate a single page into the list first. |
| assert_ok!(node.node().alloc_pages(1, 0, alloc_list.as_mut())); |
| |
| // Zero the page as a modification. |
| let front_pa = alloc_list.front().unwrap().paddr(); |
| let p_vaddr = paddr_to_physmap(front_pa); |
| // SAFETY: front_pa is a valid page allocated from PMM, so its physmap mapping is valid for |
| // page::SIZE bytes. |
| let p = unsafe { core::slice::from_raw_parts_mut(p_vaddr.0 as *mut u8, page::SIZE) }; |
| p.fill(0); |
| |
| // Now append more pages to the list. If this runs the checker on the page already in the |
| // list that we modified then it will panic. |
| expect_ok!(node.node().alloc_pages(ManagedPmmNode::NUM_PAGES / 2, 0, alloc_list.as_mut())); |
| |
| // SAFETY: alloc_list contains allocated pages. |
| unsafe { |
| node.node().free_list(alloc_list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| } |
| |
| struct WithLoanedPageWaiterArgs { |
| node: *const PmmNode, |
| page: VmPagePtr, |
| completed: *const AtomicBool, |
| seen_free_loaned: *const AtomicBool, |
| } |
| |
| // SAFETY: Raw pointers point to valid test data living for the test duration. |
| unsafe impl Send for WithLoanedPageWaiterArgs {} |
| // SAFETY: Raw pointers point to valid test data living for the test duration. |
| unsafe impl Sync for WithLoanedPageWaiterArgs {} |
| |
| extern "C" fn with_loaned_page_waiter_thread(arg: *mut core::ffi::c_void) -> i32 { |
| // SAFETY: arg is a valid pointer to WithLoanedPageWaiterArgs passed during thread spawn. |
| let args = unsafe { &*(arg as *const WithLoanedPageWaiterArgs) }; |
| // SAFETY: WithLoanedPageWaiterArgs fields point to valid objects living for the test |
| // duration. |
| let node = unsafe { &*args.node }; |
| // SAFETY: WithLoanedPageWaiterArgs fields point to valid objects living for the test |
| // duration. |
| let completed = unsafe { &*args.completed }; |
| // SAFETY: WithLoanedPageWaiterArgs fields point to valid objects living for the test |
| // duration. |
| let seen_free_loaned = unsafe { &*args.seen_free_loaned }; |
| |
| node.with_loaned_page(args.page, |p| { |
| // SAFETY: p is a valid loaned page pointer. |
| seen_free_loaned.store(unsafe { p.is_free_loaned() }, Ordering::Relaxed); |
| completed.store(true, Ordering::Relaxed); |
| }); |
| 0 |
| } |
| |
| /// Verifies `with_loaned_page` waiting and non-waiting behavior. |
| #[test] |
| fn node_with_loaned_page() { |
| stack_try_pin_init!(let node = ManagedPmmNode::init()); |
| let mut node = unwrap_ok!(node); |
| assert_ok!(node.as_mut().setup()); |
| |
| let _cleanup = ScopedLoaningEnabled::new(true); |
| |
| stack_pin_init!(let list = VmPageDoublyLinkedList::new()); |
| assert_ok!(node.node().alloc_pages(1, 0, list.as_mut())); |
| // SAFETY: list contains valid allocated page to loan. |
| unsafe { |
| node.node().begin_loan(list.as_mut(), PmmOptDelayReuse::Default); |
| } |
| |
| let mut loaned_pages = [None; 1]; |
| assert_ok!(node.alloc_loaned_pages(1, &mut loaned_pages)); |
| let page = loaned_pages[0].unwrap(); |
| |
| // 1. Page is in OBJECT state (not ALLOC); `with_loaned_page` should run immediately. |
| let mut called_object = false; |
| node.node().with_loaned_page(page, |p| { |
| assert_eq!(p.state(), VmPageState(vm_page_state::OBJECT)); |
| called_object = true; |
| }); |
| expect_true!(called_object); |
| |
| // 2. Begin freeing the loaned page into a `FreeLoanedPagesHolder`. |
| // The page is now in ALLOC state with non-null `alloc.owner` pointing to `flph`. |
| stack_pin_init!(let flph = FreeLoanedPagesHolder::init()); |
| // SAFETY: page was allocated as loaned and flph is a valid pinned holder. |
| unsafe { |
| node.node().begin_free_loaned_page( |
| page, |
| |p| crate::vm::pmm::node().page_queues().remove(p), |
| flph.as_mut(), |
| ); |
| } |
| |
| let completed = AtomicBool::new(false); |
| let seen_free_loaned = AtomicBool::new(false); |
| let args = WithLoanedPageWaiterArgs { |
| node: node.node() as *const _, |
| page, |
| completed: &completed as *const _, |
| seen_free_loaned: &seen_free_loaned as *const _, |
| }; |
| |
| // Start a thread that calls `with_loaned_page`. It must block until |
| // `finish_free_loaned_pages`. |
| // SAFETY: args outlives the spawned thread which is joined before test exit. |
| let thread = unwrap_ok!(unsafe { |
| thread::spawn( |
| c"with_loaned_page waiter".as_ptr(), |
| with_loaned_page_waiter_thread, |
| &args as *const _ as *mut core::ffi::c_void, |
| ) |
| }); |
| |
| // Give the thread time to enter `with_loaned_page` and wait on `flph`. |
| let _ = thread::sleep_relative(DurationMono::from_millis(50)); |
| |
| // The callback must NOT have run yet because `flph` still holds the page. |
| expect_false!(completed.load(Ordering::Relaxed)); |
| |
| // Finish freeing loaned pages, which wakes up the waiter thread. |
| node.node().finish_free_loaned_pages(flph.as_mut()); |
| |
| // Wait for the thread to complete. |
| // SAFETY: thread is a valid Thread handle. |
| let _ = unsafe { thread.join(InstantMono::INFINITE) }; |
| |
| expect_true!(completed.load(Ordering::Relaxed)); |
| expect_true!(seen_free_loaned.load(Ordering::Relaxed)); |
| |
| // 3. Cancel the loan so the page is removed from `free_loaned_list`. |
| // SAFETY: page is a valid loaned page pointer. |
| unsafe { |
| node.node().cancel_loan(page); |
| } |
| |
| // Test `with_loaned_page` when the page is in ALLOC state with a null `alloc.owner` |
| // (i.e. not owned by a `FreeLoanedPagesHolder`). It should run immediately without |
| // dereferencing a null pointer. |
| // SAFETY: page is exclusively owned by this test while loan is cancelled. |
| unsafe { |
| page.set_state(VmPageState(vm_page_state::ALLOC)); |
| (*page.as_ref().state_union.get()).alloc.owner = core::ptr::null_mut(); |
| } |
| let mut called_alloc_null_owner = false; |
| node.node().with_loaned_page(page, |p| { |
| assert_eq!(p.state(), VmPageState(vm_page_state::ALLOC)); |
| called_alloc_null_owner = true; |
| }); |
| expect_true!(called_alloc_null_owner); |
| |
| // Restore FREE_LOANED state to end the loan and return the page to the node. |
| // SAFETY: restoring page state before ending loan and freeing page. |
| unsafe { |
| page.set_state(VmPageState(vm_page_state::FREE_LOANED)); |
| node.node().end_loan(page); |
| node.node().free_page(page, PmmOptDelayReuse::Default); |
| } |
| } |
| } |