|
| 1 | +// Copyright 2025. Jonas Kruckenberg |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or |
| 4 | +// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or |
| 5 | +// http://opensource.org/licenses/MIT>, at your option. This file may not be |
| 6 | +// copied, modified, or distributed except according to those terms. |
| 7 | + |
| 8 | +#![no_std] |
| 9 | + |
| 10 | +//! The algorithm we use here - loosely based on Zircon's (Fuchsia's) implementation - is |
| 11 | +//! guaranteed to find a spot (if any even exist) with max 2 attempts. Additionally, it works |
| 12 | +//! elegantly *with* AND *without* ASLR, picking a random spot or the lowest free spot respectively. |
| 13 | +//! Here is how it works: |
| 14 | +//! 1. We set up two counters: |
| 15 | +//! - `candidate_spot_count` which we initialize to zero |
| 16 | +//! - `target_index` which we either set to a random value between 0..<the maximum number of |
| 17 | +//! possible addresses in the address space if ASLR is requested OR to zero otherwise. |
| 18 | +//! 2. We then iterate over all the gaps between virtual address regions from lowest to highest looking. |
| 19 | +//! We count the number of addresses in each gap that satisfy the requested `Layout`s size and |
| 20 | +//! alignment and add that to the `candidate_spot_count`. IF the number of spots in the gap is |
| 21 | +//! greater than our chosen target index, we pick the spot at the target index and finish. |
| 22 | +//! ELSE we *decrement* the target index by the number of spots and continue to the next gap. |
| 23 | +//! 3. After we have processed all the gaps, we have EITHER found a suitable spot OR our original |
| 24 | +//! guess for `target_index` was too big, in which case we need to retry. |
| 25 | +//! 4. When retrying we iterate over all gaps between virtual address regions *again*, but this time |
| 26 | +//! we know the *actual* number of possible spots in the address space since we just counted them |
| 27 | +//! during the first attempt. We initialize `target_index` to `0..candidate_spot_count` |
| 28 | +//! which is guaranteed to return us a spot. |
| 29 | +//! IF `candidate_spot_count` is ZERO after the first attempt, there is no point in |
| 30 | +//! retrying since we cannot fulfill the requested layout. |
| 31 | +//! |
| 32 | +//! Note that in practice, we use a binary tree to keep track of regions, and we use binary search |
| 33 | +//! to optimize the search for a suitable gap instead of linear iteration. |
| 34 | +
|
| 35 | +use core::alloc::Layout; |
| 36 | +use core::marker::PhantomData; |
| 37 | +use core::ops::Range; |
| 38 | + |
| 39 | +use kmem_core::{AddressRangeExt, Arch, VirtualAddress}; |
| 40 | +use rand::Rng; |
| 41 | +use rand::distr::Uniform; |
| 42 | +use rand_chacha::ChaCha20Rng; |
| 43 | + |
| 44 | +pub struct Randomizer<A: Arch> { |
| 45 | + rng: Option<ChaCha20Rng>, |
| 46 | + _arch: PhantomData<A>, |
| 47 | +} |
| 48 | + |
| 49 | +impl<A: Arch> Randomizer<A> { |
| 50 | + pub const fn new(rng: Option<ChaCha20Rng>) -> Self { |
| 51 | + Self { |
| 52 | + rng, |
| 53 | + _arch: PhantomData, |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + /// Find a spot in the given `gaps` that satisfies the given `layout` requirements. |
| 58 | + /// |
| 59 | + /// If a spot suitable for holding data described by `layout` is found, the base address of the |
| 60 | + /// address range is returned in `Some`. The returned address is already correct aligned to |
| 61 | + /// `layout.align()`. |
| 62 | + /// |
| 63 | + /// Returns `None` if no suitable spot was found. This *does not* mean there are no more gaps in |
| 64 | + /// the address space just that the *combination* of `layout.size()` and `layout.align()` cannot |
| 65 | + /// be satisfied *at the moment*. Calls to this method will a different size, alignment, or at a |
| 66 | + /// different time might still succeed. |
| 67 | + #[expect(clippy::missing_panics_doc, reason = "internal assert")] |
| 68 | + pub fn find_spot_in( |
| 69 | + &mut self, |
| 70 | + layout: Layout, |
| 71 | + gaps: impl Iterator<Item = Range<VirtualAddress>> + Clone, |
| 72 | + ) -> Option<VirtualAddress> { |
| 73 | + let layout = layout.align_to(A::GRANULE_SIZE).unwrap(); |
| 74 | + |
| 75 | + // First attempt: guess a random target index from all possible virtual addresses |
| 76 | + let max_candidate_spots = const { |
| 77 | + // The bits of entropy that we can pick a virtual address from. The theoretical maximum |
| 78 | + // is just the number of usable bits in a virtual address. E.g. if a given architecture |
| 79 | + // supports 48 bits virtual addresses, then our theoretical maximum entropy is 47 bits. |
| 80 | + // But, because the virtual memory subsystem doesn't deal in byte-granularity allocations |
| 81 | + // but in pages we need to subtract the number of bits that are just used to encode |
| 82 | + // the offset within a page (which is the log2 of the translation granule size). |
| 83 | + let entropy = A::VIRTUAL_ADDRESS_BITS as u32 - A::GRANULE_SIZE.ilog2(); |
| 84 | + |
| 85 | + (1 << entropy) - 1 |
| 86 | + }; |
| 87 | + |
| 88 | + let distr = Uniform::new(0, max_candidate_spots) |
| 89 | + .expect("no candidate spots in max range, this is a bug!"); |
| 90 | + let target_index: usize = self |
| 91 | + .rng |
| 92 | + .as_mut() |
| 93 | + .map(|rng| rng.sample(distr)) |
| 94 | + .unwrap_or_default(); |
| 95 | + |
| 96 | + // First attempt: visit the binary search tree to find a gap |
| 97 | + choose_spot(layout, target_index, gaps.clone()) |
| 98 | + // Second attempt: pick a new target_index that's actually fulfillable |
| 99 | + // based on the candidate spots we counted during the previous attempt |
| 100 | + .map_err(|candidate_spots| { |
| 101 | + // if we counted no suitable candidate spots during the first attempt, we cannot fulfill |
| 102 | + // the request. |
| 103 | + if candidate_spots == 0 { |
| 104 | + return None; |
| 105 | + } |
| 106 | + |
| 107 | + let distr = Uniform::new(0, candidate_spots).unwrap(); |
| 108 | + |
| 109 | + let target_index: usize = self |
| 110 | + .rng |
| 111 | + .as_mut() |
| 112 | + .map(|rng| rng.sample(distr)) |
| 113 | + .unwrap_or_default(); |
| 114 | + |
| 115 | + let chosen_spot = choose_spot(layout, target_index, gaps) |
| 116 | + .expect("There must be a chosen spot after the first attempt. This is a bug!"); |
| 117 | + |
| 118 | + Some(chosen_spot) |
| 119 | + }) |
| 120 | + .ok() |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +fn choose_spot( |
| 125 | + layout: Layout, |
| 126 | + mut target_index: usize, |
| 127 | + gaps: impl Iterator<Item = Range<VirtualAddress>>, |
| 128 | +) -> Result<VirtualAddress, usize> { |
| 129 | + let mut candidate_spots = 0; |
| 130 | + |
| 131 | + for gap in gaps { |
| 132 | + let aligned_gap = gap.align_in(layout.align()); |
| 133 | + |
| 134 | + let spot_count = spots_in_range(layout, &aligned_gap); |
| 135 | + |
| 136 | + candidate_spots += spot_count; |
| 137 | + |
| 138 | + if target_index < spot_count { |
| 139 | + return Ok(aligned_gap |
| 140 | + .start |
| 141 | + .add(target_index << layout.align().ilog2())); |
| 142 | + } else { |
| 143 | + target_index -= spot_count; |
| 144 | + } |
| 145 | + } |
| 146 | + Err(candidate_spots) |
| 147 | +} |
| 148 | + |
| 149 | +/// Returns the number of spots in the given range that satisfy the layout we require |
| 150 | +fn spots_in_range(layout: Layout, range: &Range<VirtualAddress>) -> usize { |
| 151 | + debug_assert!( |
| 152 | + range.start.is_aligned_to(layout.align()) && range.end.is_aligned_to(layout.align()) |
| 153 | + ); |
| 154 | + |
| 155 | + // ranges passed in here can become empty for a number of reasons (aligning might produce ranges |
| 156 | + // where end > start, or the range might be empty to begin with) in either case an empty |
| 157 | + // range means no spots are available |
| 158 | + if range.is_empty() { |
| 159 | + return 0; |
| 160 | + } |
| 161 | + |
| 162 | + let range_size = range.len(); |
| 163 | + if range_size >= layout.size() { |
| 164 | + ((range_size - layout.size()) >> layout.align().ilog2()) + 1 |
| 165 | + } else { |
| 166 | + 0 |
| 167 | + } |
| 168 | +} |
0 commit comments