ic_memory/runtime/
layout.rs1use super::RuntimeConstructionError;
5use crate::WASM_PAGE_SIZE_BYTES;
6use ic_platform::stable_structures::Memory;
7
8pub(super) const IDS: usize = crate::MEMORY_MANAGER_INVALID_ID as usize;
9pub(super) const VERSION: u8 = 1;
10const VERSION_OFFSET: usize = 3;
11const ALLOCATED_COUNT_OFFSET: usize = 4;
12const BUCKET_SIZE_OFFSET: usize = 6;
13const RESERVED_OFFSET: usize = 8;
14const MEMORY_SIZES_OFFSET: usize = 40;
15const MEMORY_SIZE_BYTES: usize = 8;
16pub(super) const BUCKET_CAPACITY: u16 = 32_768;
17pub(super) const BUCKETS: usize = BUCKET_CAPACITY as usize;
18pub(super) const HEADER_BYTES: usize = MEMORY_SIZES_OFFSET + IDS * MEMORY_SIZE_BYTES;
19pub(super) const METADATA_BYTES: usize = HEADER_BYTES + BUCKETS;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
29#[non_exhaustive]
30pub enum MemoryManagerLayoutError {
31 #[error("unsupported big-endian manager layout")]
33 UnsupportedByteOrder,
34 #[error("persisted bucket size is zero")]
36 ZeroBucketSize,
37 #[error("nonzero reserved manager header bytes")]
39 ReservedHeader,
40 #[error("allocated bucket count {count} exceeds 32768")]
42 BucketCount { count: u16 },
43 #[error("invalid bucket table entry at index {index}")]
45 BucketTable { index: u16 },
46 #[error("virtual extent and bucket count disagree for memory ID {id}")]
48 VirtualExtent { id: u8 },
49 #[error("physical extent {physical_pages} pages is below assigned end {required_pages}")]
51 TruncatedBacking {
52 physical_pages: u64,
53 required_pages: u64,
54 },
55 #[error("backing memory extent overflows bytes")]
57 ExtentOverflow,
58 #[error("persisted manager metadata differs from runtime authority")]
60 RuntimeMismatch,
61}
62
63pub(super) struct Layout {
64 pub metadata: [u8; METADATA_BYTES],
65 pub physical_pages: u64,
66 pub bucket_pages: u16,
67 pub allocated_buckets: u16,
68 pub pages: [u64; IDS],
69 pub buckets: [u16; IDS],
70}
71
72pub(super) struct ManagerAuthority {
76 expected: Box<[u8]>,
77 poisoned: std::cell::Cell<bool>,
78}
79
80impl ManagerAuthority {
81 pub fn capture(memory: &impl Memory) -> Self {
82 let mut expected = vec![0; METADATA_BYTES].into_boxed_slice();
83 memory.read(0, &mut expected);
84 Self {
85 expected,
86 poisoned: std::cell::Cell::new(false),
87 }
88 }
89
90 pub fn allocated_buckets(&self) -> u16 {
91 u16::from_le_bytes([
92 self.expected[ALLOCATED_COUNT_OFFSET],
93 self.expected[ALLOCATED_COUNT_OFFSET + 1],
94 ])
95 }
96
97 pub fn verify(&self, memory: &impl Memory) -> Result<(), MemoryManagerLayoutError> {
98 self.check_healthy()?;
99 #[expect(
100 clippy::large_stack_arrays,
101 reason = "fixed manager metadata bound, no payload reads or heap allocation"
102 )]
103 let mut actual = [0; METADATA_BYTES];
104 let required_pages = 1 + u64::from(self.allocated_buckets())
105 * u64::from(u16::from_le_bytes([
106 self.expected[BUCKET_SIZE_OFFSET],
107 self.expected[BUCKET_SIZE_OFFSET + 1],
108 ]));
109 if memory.size() < required_pages {
110 self.poisoned.set(true);
111 return Err(MemoryManagerLayoutError::RuntimeMismatch);
112 }
113 memory.read(0, &mut actual);
114 self.verify_observed(&actual)
115 }
116
117 pub const fn check_healthy(&self) -> Result<(), MemoryManagerLayoutError> {
118 if self.poisoned.get() {
119 Err(MemoryManagerLayoutError::RuntimeMismatch)
120 } else {
121 Ok(())
122 }
123 }
124
125 pub fn poison(&self) {
126 self.poisoned.set(true);
127 }
128
129 pub fn verify_observed(&self, actual: &[u8]) -> Result<(), MemoryManagerLayoutError> {
130 #[cfg(target_arch = "wasm32")]
134 let matches = {
135 let (actual_words, actual_tail) = actual.as_chunks::<8>();
136 let (expected_words, expected_tail) = self.expected.as_chunks::<8>();
137 actual.len() == METADATA_BYTES
138 && self.expected.len() == METADATA_BYTES
139 && actual_tail.is_empty()
140 && expected_tail.is_empty()
141 && actual_words
142 .iter()
143 .zip(expected_words)
144 .all(|(actual, expected)| {
145 u64::from_le_bytes(*actual) == u64::from_le_bytes(*expected)
146 })
147 };
148 #[cfg(not(target_arch = "wasm32"))]
151 let matches = actual.len() == METADATA_BYTES && actual == self.expected.as_ref();
152 if self.poisoned.get() || !matches {
153 self.poisoned.set(true);
154 return Err(MemoryManagerLayoutError::RuntimeMismatch);
155 }
156 Ok(())
157 }
158
159 pub fn record_growth(&mut self, id: u8, pages: u64, allocated: u16) {
160 let old = usize::from(self.allocated_buckets());
161 self.expected[HEADER_BYTES + old..HEADER_BYTES + usize::from(allocated)].fill(id);
162 self.expected[ALLOCATED_COUNT_OFFSET..BUCKET_SIZE_OFFSET]
163 .copy_from_slice(&allocated.to_le_bytes());
164 let offset = memory_size_offset(id);
165 self.expected[offset..offset + MEMORY_SIZE_BYTES].copy_from_slice(&pages.to_le_bytes());
166 }
167}
168
169const fn memory_size_offset(id: u8) -> usize {
171 MEMORY_SIZES_OFFSET + id as usize * MEMORY_SIZE_BYTES
172}
173
174pub(super) fn read<M: Memory>(memory: &M) -> Result<Layout, RuntimeConstructionError> {
175 if cfg!(target_endian = "big") {
176 return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
177 }
178 let physical_pages = memory.size();
179 physical_pages
180 .checked_mul(WASM_PAGE_SIZE_BYTES)
181 .ok_or(MemoryManagerLayoutError::ExtentOverflow)?;
182 if physical_pages == 0 {
184 return Err(MemoryManagerLayoutError::TruncatedBacking {
185 physical_pages,
186 required_pages: 1,
187 }
188 .into());
189 }
190 #[expect(
191 clippy::large_stack_arrays,
192 reason = "bounded manager image is also the diagnostic authority observation"
193 )]
194 let mut metadata = [0; METADATA_BYTES];
195 let (header, table) = metadata.split_at_mut(HEADER_BYTES);
196 memory.read(0, header);
197 let observed_magic = [header[0], header[1], header[2]];
198 if observed_magic != *b"MGR" {
199 return Err(RuntimeConstructionError::ForeignMemory { observed_magic });
200 }
201 if header[VERSION_OFFSET] != VERSION {
202 return Err(RuntimeConstructionError::UnsupportedMemoryManagerVersion {
203 observed: header[VERSION_OFFSET],
204 supported: VERSION,
205 });
206 }
207 if header[RESERVED_OFFSET..MEMORY_SIZES_OFFSET]
208 .iter()
209 .any(|byte| *byte != 0)
210 {
211 return Err(MemoryManagerLayoutError::ReservedHeader.into());
212 }
213 let allocated_buckets = u16::from_le_bytes([
214 header[ALLOCATED_COUNT_OFFSET],
215 header[ALLOCATED_COUNT_OFFSET + 1],
216 ]);
217 let bucket_pages =
218 u16::from_le_bytes([header[BUCKET_SIZE_OFFSET], header[BUCKET_SIZE_OFFSET + 1]]);
219 if bucket_pages == 0 {
220 return Err(MemoryManagerLayoutError::ZeroBucketSize.into());
221 }
222 if usize::from(allocated_buckets) > BUCKETS {
223 return Err(MemoryManagerLayoutError::BucketCount {
224 count: allocated_buckets,
225 }
226 .into());
227 }
228 let required_pages = 1 + u64::from(allocated_buckets) * u64::from(bucket_pages);
229 if physical_pages < required_pages {
230 return Err(MemoryManagerLayoutError::TruncatedBacking {
231 physical_pages,
232 required_pages,
233 }
234 .into());
235 }
236 memory.read(HEADER_BYTES as u64, table);
238 let mut buckets = [0_u16; IDS];
239 for (index, id) in (0..BUCKET_CAPACITY).zip(table.iter().copied()) {
240 if (index < allocated_buckets) != (id != crate::MEMORY_MANAGER_INVALID_ID) {
241 return Err(MemoryManagerLayoutError::BucketTable { index }.into());
242 }
243 if id != crate::MEMORY_MANAGER_INVALID_ID {
244 buckets[usize::from(id)] += 1;
245 }
246 }
247 let mut pages = [0; IDS];
248 for (id, value) in (0..crate::MEMORY_MANAGER_INVALID_ID).zip(&mut pages) {
249 let offset = memory_size_offset(id);
250 *value = u64::from_le_bytes(
251 header[offset..offset + MEMORY_SIZE_BYTES]
252 .try_into()
253 .expect("eight bytes"),
254 );
255 if value.div_ceil(u64::from(bucket_pages)) != u64::from(buckets[usize::from(id)]) {
256 return Err(MemoryManagerLayoutError::VirtualExtent { id }.into());
257 }
258 }
259 Ok(Layout {
260 metadata,
261 physical_pages,
262 bucket_pages,
263 allocated_buckets,
264 pages,
265 buckets,
266 })
267}