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