gear_core/
memory.rs

1// This file is part of Gear.
2
3// Copyright (C) 2021-2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Module for memory and allocations context.
20
21use crate::{
22    buffer::LimitedVec,
23    gas::ChargeError,
24    pages::{GearPage, WasmPage, WasmPagesAmount},
25};
26use alloc::format;
27use byteorder::{ByteOrder, LittleEndian};
28use core::{
29    fmt,
30    fmt::Debug,
31    ops::{Deref, DerefMut},
32};
33use numerated::{
34    interval::{Interval, TryFromRangeError},
35    tree::IntervalsTree,
36};
37use scale_info::{
38    TypeInfo,
39    scale::{self, Decode, Encode, EncodeLike, Input, Output},
40};
41
42/// Interval in wasm program memory.
43#[derive(Clone, Copy, Eq, PartialEq, Encode, Decode)]
44pub struct MemoryInterval {
45    /// Interval offset in bytes.
46    pub offset: u32,
47    /// Interval size in bytes.
48    pub size: u32,
49}
50
51impl MemoryInterval {
52    /// Convert `MemoryInterval` to `[u8; 8]` bytes.
53    /// `0..4` - `offset`
54    /// `4..8` - `size`
55    #[inline]
56    pub fn to_bytes(&self) -> [u8; 8] {
57        let mut bytes = [0u8; 8];
58        LittleEndian::write_u32(&mut bytes[0..4], self.offset);
59        LittleEndian::write_u32(&mut bytes[4..8], self.size);
60        bytes
61    }
62
63    /// Convert `[u8; 8]` bytes to `MemoryInterval`.
64    /// `0..4` - `offset`
65    /// `4..8` - `size`
66    #[inline]
67    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
68        if bytes.len() != 8 {
69            return Err("bytes size != 8");
70        }
71        let offset = LittleEndian::read_u32(&bytes[0..4]);
72        let size = LittleEndian::read_u32(&bytes[4..8]);
73        Ok(MemoryInterval { offset, size })
74    }
75}
76
77impl From<(u32, u32)> for MemoryInterval {
78    fn from(val: (u32, u32)) -> Self {
79        MemoryInterval {
80            offset: val.0,
81            size: val.1,
82        }
83    }
84}
85
86impl From<MemoryInterval> for (u32, u32) {
87    fn from(val: MemoryInterval) -> Self {
88        (val.offset, val.size)
89    }
90}
91
92impl Debug for MemoryInterval {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.debug_struct("MemoryInterval")
95            .field("offset", &format_args!("{:#x}", self.offset))
96            .field("size", &format_args!("{:#x}", self.size))
97            .finish()
98    }
99}
100
101/// Error in attempt to make wrong size page buffer.
102#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, TypeInfo, derive_more::Display)]
103#[display("Trying to make wrong size page buffer, must be {:#x}", GearPage::SIZE)]
104pub struct IntoPageBufError;
105
106/// Alias for inner type of page buffer.
107pub type PageBufInner = LimitedVec<u8, IntoPageBufError, { GearPage::SIZE as usize }>;
108
109/// Buffer for gear page data.
110#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, TypeInfo)]
111pub struct PageBuf(PageBufInner);
112
113// These traits are implemented intentionally by hand to achieve two goals:
114// - store PageBuf as fixed size array in a storage to eliminate extra bytes
115//      for length;
116// - work with PageBuf as with Vec. This is to workaround a limit in 2_048
117//      items for fixed length array in polkadot.js/metadata.
118//      Grep 'Only support for [[]Type' to get more details on that.
119impl Encode for PageBuf {
120    fn size_hint(&self) -> usize {
121        GearPage::SIZE as usize
122    }
123
124    fn encode_to<W: Output + ?Sized>(&self, dest: &mut W) {
125        dest.write(self.0.inner())
126    }
127}
128
129impl Decode for PageBuf {
130    #[inline]
131    fn decode<I: Input>(input: &mut I) -> Result<Self, scale::Error> {
132        let mut buffer = PageBufInner::new_default();
133        input.read(buffer.inner_mut())?;
134        Ok(Self(buffer))
135    }
136}
137
138impl EncodeLike for PageBuf {}
139
140impl Debug for PageBuf {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(
143            f,
144            "PageBuf({:?}..{:?})",
145            &self.0.inner()[0..10],
146            &self.0.inner()[GearPage::SIZE as usize - 10..GearPage::SIZE as usize]
147        )
148    }
149}
150
151impl Deref for PageBuf {
152    type Target = [u8];
153    fn deref(&self) -> &Self::Target {
154        self.0.inner()
155    }
156}
157
158impl DerefMut for PageBuf {
159    fn deref_mut(&mut self) -> &mut Self::Target {
160        self.0.inner_mut()
161    }
162}
163
164impl PageBuf {
165    /// Returns new page buffer with zeroed data.
166    pub fn new_zeroed() -> PageBuf {
167        Self(PageBufInner::new_default())
168    }
169
170    /// Creates PageBuf from inner buffer. If the buffer has
171    /// the size of [`GearPage`] then no reallocations occur.
172    /// In other case it will be extended with zeros.
173    ///
174    /// The method is implemented intentionally instead of trait From to
175    /// highlight conversion cases in the source code.
176    pub fn from_inner(mut inner: PageBufInner) -> Self {
177        inner.extend_with(0);
178        Self(inner)
179    }
180}
181
182/// Host pointer type.
183/// Host pointer can be 64bit or less, to support both we use u64.
184pub type HostPointer = u64;
185
186const _: () = assert!(size_of::<HostPointer>() >= size_of::<usize>());
187
188/// Core memory error.
189#[derive(Debug, Copy, Clone, Eq, PartialEq, derive_more::Display)]
190pub enum MemoryError {
191    /// The error occurs in attempt to access memory outside wasm program memory.
192    #[display("Trying to access memory outside wasm program memory")]
193    AccessOutOfBounds,
194}
195
196/// Backend wasm memory interface.
197pub trait Memory<Context> {
198    /// Memory grow error.
199    type GrowError: Debug;
200
201    /// Grow memory by number of pages.
202    fn grow(&self, ctx: &mut Context, pages: WasmPagesAmount) -> Result<(), Self::GrowError>;
203
204    /// Return current size of the memory.
205    fn size(&self, ctx: &Context) -> WasmPagesAmount;
206
207    /// Set memory region at specific pointer.
208    fn write(&self, ctx: &mut Context, offset: u32, buffer: &[u8]) -> Result<(), MemoryError>;
209
210    /// Reads memory contents at the given offset into a buffer.
211    fn read(&self, ctx: &Context, offset: u32, buffer: &mut [u8]) -> Result<(), MemoryError>;
212
213    /// Returns native addr of wasm memory buffer in wasm executor
214    fn get_buffer_host_addr(&self, ctx: &Context) -> Option<HostPointer> {
215        if self.size(ctx) == WasmPagesAmount::from(0) {
216            None
217        } else {
218            // We call this method only in case memory size is not zero,
219            // so memory buffer exists and has addr in host memory.
220            unsafe { Some(self.get_buffer_host_addr_unsafe(ctx)) }
221        }
222    }
223
224    /// Get buffer addr unsafe.
225    ///
226    /// # Safety
227    /// If memory size is 0 then buffer addr can be garbage
228    unsafe fn get_buffer_host_addr_unsafe(&self, ctx: &Context) -> HostPointer;
229}
230
231/// Pages allocations context for the running program.
232#[derive(Debug)]
233pub struct AllocationsContext {
234    /// Pages which has been in storage before execution
235    allocations: IntervalsTree<WasmPage>,
236    /// Shows that `allocations` was modified at least once per execution
237    allocations_changed: bool,
238    heap: Option<Interval<WasmPage>>,
239    static_pages: WasmPagesAmount,
240}
241
242/// Before and after memory grow actions.
243#[must_use]
244pub trait GrowHandler<Context> {
245    /// Before grow action
246    fn before_grow_action(ctx: &mut Context, mem: &mut impl Memory<Context>) -> Self;
247    /// After grow action
248    fn after_grow_action(self, ctx: &mut Context, mem: &mut impl Memory<Context>);
249}
250
251/// Grow handler do nothing implementation
252pub struct NoopGrowHandler;
253
254impl<Context> GrowHandler<Context> for NoopGrowHandler {
255    fn before_grow_action(_ctx: &mut Context, _mem: &mut impl Memory<Context>) -> Self {
256        NoopGrowHandler
257    }
258    fn after_grow_action(self, _ctx: &mut Context, _mem: &mut impl Memory<Context>) {}
259}
260
261/// Inconsistency in memory parameters provided for wasm execution.
262#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
263pub enum MemorySetupError {
264    /// Memory size exceeds max pages
265    #[display("Memory size {memory_size:?} must be less than or equal to {max_pages:?}")]
266    MemorySizeExceedsMaxPages {
267        /// Memory size
268        memory_size: WasmPagesAmount,
269        /// Max allowed memory size
270        max_pages: WasmPagesAmount,
271    },
272    /// Insufficient memory size
273    #[display("Memory size {memory_size:?} must be at least {static_pages:?}")]
274    InsufficientMemorySize {
275        /// Memory size
276        memory_size: WasmPagesAmount,
277        /// Static memory size
278        static_pages: WasmPagesAmount,
279    },
280    /// Stack end is out of static memory
281    #[display("Stack end {stack_end:?} is out of static memory 0..{static_pages:?}")]
282    StackEndOutOfStaticMemory {
283        /// Stack end
284        stack_end: WasmPage,
285        /// Static memory size
286        static_pages: WasmPagesAmount,
287    },
288    /// Allocated page is out of allowed memory interval
289    #[display(
290        "Allocated page {page:?} is out of allowed memory interval {static_pages:?}..{memory_size:?}"
291    )]
292    AllocatedPageOutOfAllowedInterval {
293        /// Allocated page
294        page: WasmPage,
295        /// Static memory size
296        static_pages: WasmPagesAmount,
297        /// Memory size
298        memory_size: WasmPagesAmount,
299    },
300}
301
302/// Allocation error
303#[derive(Debug, Clone, Eq, PartialEq, derive_more::Display, derive_more::From)]
304pub enum AllocError {
305    /// The error occurs when a program tries to allocate more memory than
306    /// allowed.
307    #[display("Trying to allocate more wasm program memory than allowed")]
308    ProgramAllocOutOfBounds,
309    /// The error occurs in attempt to free-up a memory page from static area or
310    /// outside additionally allocated for this program.
311    #[display("{_0:?} cannot be freed by the current program")]
312    InvalidFree(WasmPage),
313    /// Invalid range for free_range
314    #[display("Invalid range {_0:?}..={_1:?} for free_range")]
315    InvalidFreeRange(WasmPage, WasmPage),
316    /// Gas charge error
317    GasCharge(ChargeError),
318}
319
320impl AllocationsContext {
321    /// New allocations context.
322    ///
323    /// Provide currently running `program_id`, boxed memory abstraction
324    /// and allocation manager. Also configurable `static_pages` and `max_pages`
325    /// are set.
326    ///
327    /// Returns `MemorySetupError` on incorrect memory params.
328    pub fn try_new(
329        memory_size: WasmPagesAmount,
330        allocations: IntervalsTree<WasmPage>,
331        static_pages: WasmPagesAmount,
332        stack_end: Option<WasmPage>,
333        max_pages: WasmPagesAmount,
334    ) -> Result<Self, MemorySetupError> {
335        Self::validate_memory_params(
336            memory_size,
337            &allocations,
338            static_pages,
339            stack_end,
340            max_pages,
341        )?;
342
343        let heap = match Interval::try_from(static_pages..max_pages) {
344            Ok(interval) => Some(interval),
345            Err(TryFromRangeError::EmptyRange) => None,
346            // Branch is unreachable due to the check `static_pages <= max_pages`` in `validate_memory_params`.
347            _ => unreachable!(),
348        };
349
350        Ok(Self {
351            allocations,
352            allocations_changed: false,
353            heap,
354            static_pages,
355        })
356    }
357
358    /// Checks memory parameters, that are provided for wasm execution.
359    /// NOTE: this params partially checked in `Code::try_new` in `gear-core`.
360    fn validate_memory_params(
361        memory_size: WasmPagesAmount,
362        allocations: &IntervalsTree<WasmPage>,
363        static_pages: WasmPagesAmount,
364        stack_end: Option<WasmPage>,
365        max_pages: WasmPagesAmount,
366    ) -> Result<(), MemorySetupError> {
367        if memory_size > max_pages {
368            return Err(MemorySetupError::MemorySizeExceedsMaxPages {
369                memory_size,
370                max_pages,
371            });
372        }
373
374        if static_pages > memory_size {
375            return Err(MemorySetupError::InsufficientMemorySize {
376                memory_size,
377                static_pages,
378            });
379        }
380
381        if let Some(stack_end) = stack_end
382            && stack_end > static_pages
383        {
384            return Err(MemorySetupError::StackEndOutOfStaticMemory {
385                stack_end,
386                static_pages,
387            });
388        }
389
390        if let Some(page) = allocations.end()
391            && page >= memory_size
392        {
393            return Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
394                page,
395                static_pages,
396                memory_size,
397            });
398        }
399        if let Some(page) = allocations.start()
400            && page < static_pages
401        {
402            return Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
403                page,
404                static_pages,
405                memory_size,
406            });
407        }
408
409        Ok(())
410    }
411
412    /// Allocates specified number of continuously going pages
413    /// and returns zero-based number of the first one.
414    pub fn alloc<Context, G: GrowHandler<Context>>(
415        &mut self,
416        ctx: &mut Context,
417        mem: &mut impl Memory<Context>,
418        pages: WasmPagesAmount,
419        charge_gas_for_grow: impl FnOnce(WasmPagesAmount) -> Result<(), ChargeError>,
420    ) -> Result<WasmPage, AllocError> {
421        // Empty heap means that all memory is static, then no pages can be allocated.
422        // NOTE: returns an error even if `pages` == 0.
423        let heap = self.heap.ok_or(AllocError::ProgramAllocOutOfBounds)?;
424
425        // If trying to allocate zero pages, then returns heap start page (legacy).
426        if pages == WasmPage::from(0) {
427            return Ok(heap.start());
428        }
429
430        let interval = self
431            .allocations
432            .voids(heap)
433            .find_map(|void| {
434                Interval::<WasmPage>::with_len(void.start(), u32::from(pages))
435                    .ok()
436                    .and_then(|interval| (interval.end() <= void.end()).then_some(interval))
437            })
438            .ok_or(AllocError::ProgramAllocOutOfBounds)?;
439
440        if let Ok(grow) = Interval::<WasmPage>::try_from(mem.size(ctx)..interval.end().inc()) {
441            charge_gas_for_grow(grow.len())?;
442            let grow_handler = G::before_grow_action(ctx, mem);
443            mem.grow(ctx, grow.len()).unwrap_or_else(|err| {
444                let err_msg = format!(
445                    "AllocationContext:alloc: Failed to grow memory. \
446                        Got error - {err:?}",
447                );
448
449                log::error!("{err_msg}");
450                unreachable!("{err_msg}")
451            });
452            grow_handler.after_grow_action(ctx, mem);
453        }
454
455        self.allocations.insert(interval);
456        self.allocations_changed = true;
457
458        Ok(interval.start())
459    }
460
461    /// Free specific memory page.
462    pub fn free(&mut self, page: WasmPage) -> Result<(), AllocError> {
463        if let Some(heap) = self.heap
464            && page >= heap.start()
465            && page <= heap.end()
466            && self.allocations.remove(page)
467        {
468            self.allocations_changed = true;
469            return Ok(());
470        }
471
472        Err(AllocError::InvalidFree(page))
473    }
474
475    /// Try to free pages in range. Will only return error if range is invalid.
476    ///
477    /// Currently running program should own this pages.
478    pub fn free_range(&mut self, interval: Interval<WasmPage>) -> Result<(), AllocError> {
479        if let Some(heap) = self.heap {
480            // `free_range` allows do not modify the allocations so we do not check the `remove` result here
481            if interval.start() >= heap.start() && interval.end() <= heap.end() {
482                if self.allocations.remove(interval) {
483                    self.allocations_changed = true;
484                }
485
486                return Ok(());
487            }
488        }
489
490        Err(AllocError::InvalidFreeRange(
491            interval.start(),
492            interval.end(),
493        ))
494    }
495
496    /// Decomposes this instance and returns `static_pages`, `allocations` and `allocations_changed` params.
497    pub fn into_parts(self) -> (WasmPagesAmount, IntervalsTree<WasmPage>, bool) {
498        (
499            self.static_pages,
500            self.allocations,
501            self.allocations_changed,
502        )
503    }
504}
505
506/// This module contains tests of `GearPage` and `AllocationContext`
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use alloc::vec::Vec;
511    use core::{cell::Cell, iter};
512
513    struct TestMemory(Cell<WasmPagesAmount>);
514
515    impl TestMemory {
516        fn new(amount: WasmPagesAmount) -> Self {
517            Self(Cell::new(amount))
518        }
519    }
520
521    impl Memory<()> for TestMemory {
522        type GrowError = ();
523
524        fn grow(&self, _ctx: &mut (), pages: WasmPagesAmount) -> Result<(), Self::GrowError> {
525            let new_pages_amount = self.0.get().add(pages).ok_or(())?;
526            self.0.set(new_pages_amount);
527            Ok(())
528        }
529
530        fn size(&self, _ctx: &()) -> WasmPagesAmount {
531            self.0.get()
532        }
533
534        fn write(&self, _ctx: &mut (), _offset: u32, _buffer: &[u8]) -> Result<(), MemoryError> {
535            unimplemented!()
536        }
537
538        fn read(&self, _ctx: &(), _offset: u32, _buffer: &mut [u8]) -> Result<(), MemoryError> {
539            unimplemented!()
540        }
541
542        unsafe fn get_buffer_host_addr_unsafe(&self, _ctx: &()) -> HostPointer {
543            unimplemented!()
544        }
545    }
546
547    #[test]
548    fn page_buf() {
549        let _ = tracing_subscriber::fmt::try_init();
550
551        let mut data = PageBufInner::filled_with(199u8);
552        data.inner_mut()[1] = 2;
553        let page_buf = PageBuf::from_inner(data);
554        log::debug!("page buff = {page_buf:?}");
555    }
556
557    #[test]
558    fn free_fails() {
559        let mut ctx =
560            AllocationsContext::try_new(0.into(), Default::default(), 0.into(), None, 0.into())
561                .unwrap();
562        assert_eq!(ctx.free(1.into()), Err(AllocError::InvalidFree(1.into())));
563
564        let mut ctx = AllocationsContext::try_new(
565            1.into(),
566            [WasmPage::from(0)].into_iter().collect(),
567            0.into(),
568            None,
569            1.into(),
570        )
571        .unwrap();
572        assert_eq!(ctx.free(1.into()), Err(AllocError::InvalidFree(1.into())));
573
574        let mut ctx = AllocationsContext::try_new(
575            4.into(),
576            [WasmPage::from(1), WasmPage::from(3)].into_iter().collect(),
577            1.into(),
578            None,
579            4.into(),
580        )
581        .unwrap();
582        let interval = Interval::<WasmPage>::try_from(1u16..4).unwrap();
583        assert_eq!(ctx.free_range(interval), Ok(()));
584    }
585
586    #[track_caller]
587    fn alloc_ok(ctx: &mut AllocationsContext, mem: &mut TestMemory, pages: u16, expected: u16) {
588        let res = ctx.alloc::<(), NoopGrowHandler>(&mut (), mem, pages.into(), |_| Ok(()));
589        assert_eq!(res, Ok(expected.into()));
590    }
591
592    #[track_caller]
593    fn alloc_err(ctx: &mut AllocationsContext, mem: &mut TestMemory, pages: u16, err: AllocError) {
594        let res = ctx.alloc::<(), NoopGrowHandler>(&mut (), mem, pages.into(), |_| Ok(()));
595        assert_eq!(res, Err(err));
596    }
597
598    #[test]
599    fn alloc() {
600        let _ = tracing_subscriber::fmt::try_init();
601
602        let mut ctx = AllocationsContext::try_new(
603            256.into(),
604            Default::default(),
605            16.into(),
606            None,
607            256.into(),
608        )
609        .unwrap();
610        let mut mem = TestMemory::new(16.into());
611        alloc_ok(&mut ctx, &mut mem, 16, 16);
612        alloc_ok(&mut ctx, &mut mem, 0, 16);
613
614        // there is a space for 14 more
615        (2..16).for_each(|i| alloc_ok(&mut ctx, &mut mem, 16, i * 16));
616
617        // no more mem!
618        alloc_err(&mut ctx, &mut mem, 16, AllocError::ProgramAllocOutOfBounds);
619
620        // but we free some and then can allocate page that was freed
621        ctx.free(137.into()).unwrap();
622        alloc_ok(&mut ctx, &mut mem, 1, 137);
623
624        // if we free 2 in a row we can allocate even 2
625        ctx.free(117.into()).unwrap();
626        ctx.free(118.into()).unwrap();
627        alloc_ok(&mut ctx, &mut mem, 2, 117);
628
629        // same as above, if we free_range 2 in a row we can allocate 2
630        let interval = Interval::<WasmPage>::try_from(117..119).unwrap();
631        ctx.free_range(interval).unwrap();
632        alloc_ok(&mut ctx, &mut mem, 2, 117);
633
634        // but if 2 are not in a row, bad luck
635        ctx.free(117.into()).unwrap();
636        ctx.free(158.into()).unwrap();
637        alloc_err(&mut ctx, &mut mem, 2, AllocError::ProgramAllocOutOfBounds);
638    }
639
640    #[test]
641    fn memory_params_validation() {
642        assert_eq!(
643            AllocationsContext::validate_memory_params(
644                4.into(),
645                &iter::once(WasmPage::from(2)).collect(),
646                2.into(),
647                Some(2.into()),
648                4.into(),
649            ),
650            Ok(())
651        );
652
653        assert_eq!(
654            AllocationsContext::validate_memory_params(
655                4.into(),
656                &Default::default(),
657                2.into(),
658                Some(2.into()),
659                3.into(),
660            ),
661            Err(MemorySetupError::MemorySizeExceedsMaxPages {
662                memory_size: 4.into(),
663                max_pages: 3.into()
664            })
665        );
666
667        assert_eq!(
668            AllocationsContext::validate_memory_params(
669                1.into(),
670                &Default::default(),
671                2.into(),
672                Some(1.into()),
673                4.into(),
674            ),
675            Err(MemorySetupError::InsufficientMemorySize {
676                memory_size: 1.into(),
677                static_pages: 2.into()
678            })
679        );
680
681        assert_eq!(
682            AllocationsContext::validate_memory_params(
683                4.into(),
684                &Default::default(),
685                2.into(),
686                Some(3.into()),
687                4.into(),
688            ),
689            Err(MemorySetupError::StackEndOutOfStaticMemory {
690                stack_end: 3.into(),
691                static_pages: 2.into()
692            })
693        );
694
695        assert_eq!(
696            AllocationsContext::validate_memory_params(
697                4.into(),
698                &[WasmPage::from(1), WasmPage::from(3)].into_iter().collect(),
699                2.into(),
700                Some(2.into()),
701                4.into(),
702            ),
703            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
704                page: 1.into(),
705                static_pages: 2.into(),
706                memory_size: 4.into()
707            })
708        );
709
710        assert_eq!(
711            AllocationsContext::validate_memory_params(
712                4.into(),
713                &[WasmPage::from(2), WasmPage::from(4)].into_iter().collect(),
714                2.into(),
715                Some(2.into()),
716                4.into(),
717            ),
718            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
719                page: 4.into(),
720                static_pages: 2.into(),
721                memory_size: 4.into()
722            })
723        );
724
725        assert_eq!(
726            AllocationsContext::validate_memory_params(
727                13.into(),
728                &iter::once(WasmPage::from(1)).collect(),
729                10.into(),
730                None,
731                13.into()
732            ),
733            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
734                page: 1.into(),
735                static_pages: 10.into(),
736                memory_size: 13.into()
737            })
738        );
739
740        assert_eq!(
741            AllocationsContext::validate_memory_params(
742                13.into(),
743                &iter::once(WasmPage::from(1)).collect(),
744                WasmPagesAmount::UPPER,
745                None,
746                13.into()
747            ),
748            Err(MemorySetupError::InsufficientMemorySize {
749                memory_size: 13.into(),
750                static_pages: WasmPagesAmount::UPPER
751            })
752        );
753
754        assert_eq!(
755            AllocationsContext::validate_memory_params(
756                WasmPagesAmount::UPPER,
757                &iter::once(WasmPage::from(1)).collect(),
758                10.into(),
759                None,
760                WasmPagesAmount::UPPER,
761            ),
762            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
763                page: 1.into(),
764                static_pages: 10.into(),
765                memory_size: WasmPagesAmount::UPPER
766            })
767        );
768    }
769
770    #[test]
771    fn allocations_changed_correctness() {
772        let new_ctx = |allocations| {
773            AllocationsContext::try_new(16.into(), allocations, 0.into(), None, 16.into()).unwrap()
774        };
775
776        // correct `alloc`
777        let mut ctx = new_ctx(Default::default());
778        assert!(
779            !ctx.allocations_changed,
780            "Expecting no changes after creation"
781        );
782        let mut mem = TestMemory::new(16.into());
783        alloc_ok(&mut ctx, &mut mem, 16, 0);
784        assert!(ctx.allocations_changed);
785
786        let (_, allocations, allocations_changed) = ctx.into_parts();
787        assert!(allocations_changed);
788
789        // fail `alloc`
790        let mut ctx = new_ctx(allocations);
791        alloc_err(&mut ctx, &mut mem, 16, AllocError::ProgramAllocOutOfBounds);
792        assert!(
793            !ctx.allocations_changed,
794            "Expecting allocations don't change because of error"
795        );
796
797        // fail `free`
798        assert!(ctx.free(16.into()).is_err());
799        assert!(!ctx.allocations_changed);
800
801        // correct `free`
802        assert!(ctx.free(10.into()).is_ok());
803        assert!(ctx.allocations_changed);
804
805        let (_, allocations, allocations_changed) = ctx.into_parts();
806        assert!(allocations_changed);
807
808        // correct `free_range`
809        // allocations: [0..9] ∪ [11..15]
810        let mut ctx = new_ctx(allocations);
811        let interval = Interval::<WasmPage>::try_from(10u16..12).unwrap();
812        assert!(ctx.free_range(interval).is_ok());
813        assert!(
814            ctx.allocations_changed,
815            "Expected value is `true` because the 11th page was freed from allocations."
816        );
817
818        let (_, allocations, allocations_changed) = ctx.into_parts();
819        assert!(allocations_changed);
820
821        // fail `free_range`
822        let mut ctx = new_ctx(allocations);
823        let interval = Interval::<WasmPage>::try_from(0u16..17).unwrap();
824        assert!(ctx.free_range(interval).is_err());
825        assert!(!ctx.allocations_changed);
826        assert!(!ctx.into_parts().2);
827    }
828
829    mod property_tests {
830        use super::*;
831        use proptest::{
832            arbitrary::any,
833            collection::size_range,
834            prop_oneof, proptest,
835            strategy::{Just, Strategy},
836            test_runner::Config as ProptestConfig,
837        };
838
839        #[derive(Debug, Clone)]
840        enum Action {
841            Alloc { pages: WasmPagesAmount },
842            Free { page: WasmPage },
843            FreeRange { page: WasmPage, size: u8 },
844        }
845
846        fn actions() -> impl Strategy<Value = Vec<Action>> {
847            let action = prop_oneof![
848                // Allocate smaller number (0..32) of pages due to `BTree::extend` significantly slows down prop-test.
849                wasm_pages_amount_with_range(0, 32).prop_map(|pages| Action::Alloc { pages }),
850                wasm_page().prop_map(|page| Action::Free { page }),
851                (wasm_page(), any::<u8>())
852                    .prop_map(|(page, size)| Action::FreeRange { page, size }),
853            ];
854            proptest::collection::vec(action, 0..1024)
855        }
856
857        fn allocations(start: u16, end: u16) -> impl Strategy<Value = IntervalsTree<WasmPage>> {
858            proptest::collection::btree_set(wasm_page_with_range(start, end), size_range(0..1024))
859                .prop_map(|pages| pages.into_iter().collect::<IntervalsTree<WasmPage>>())
860        }
861
862        fn wasm_page_with_range(start: u16, end: u16) -> impl Strategy<Value = WasmPage> {
863            (start..=end).prop_map(WasmPage::from)
864        }
865
866        fn wasm_page() -> impl Strategy<Value = WasmPage> {
867            wasm_page_with_range(0, u16::MAX)
868        }
869
870        fn wasm_pages_amount_with_range(
871            start: u32,
872            end: u32,
873        ) -> impl Strategy<Value = WasmPagesAmount> {
874            (start..=end).prop_map(|x| {
875                if x == u16::MAX as u32 + 1 {
876                    WasmPagesAmount::UPPER
877                } else {
878                    WasmPagesAmount::from(x as u16)
879                }
880            })
881        }
882
883        fn wasm_pages_amount() -> impl Strategy<Value = WasmPagesAmount> {
884            wasm_pages_amount_with_range(0, u16::MAX as u32 + 1)
885        }
886
887        #[derive(Debug)]
888        struct MemoryParams {
889            max_pages: WasmPagesAmount,
890            mem_size: WasmPagesAmount,
891            static_pages: WasmPagesAmount,
892            allocations: IntervalsTree<WasmPage>,
893        }
894
895        // This high-order strategy generates valid memory parameters in a specific way that allows passing `AllocationContext::validate_memory_params` checks.
896        fn combined_memory_params() -> impl Strategy<Value = MemoryParams> {
897            wasm_pages_amount()
898                .prop_flat_map(|max_pages| {
899                    let mem_size = wasm_pages_amount_with_range(0, u32::from(max_pages));
900                    (Just(max_pages), mem_size)
901                })
902                .prop_flat_map(|(max_pages, mem_size)| {
903                    let static_pages = wasm_pages_amount_with_range(0, u32::from(mem_size));
904                    (Just(max_pages), Just(mem_size), static_pages)
905                })
906                .prop_filter(
907                    "filter out cases where allocation region has zero size",
908                    |(_max_pages, mem_size, static_pages)| static_pages < mem_size,
909                )
910                .prop_flat_map(|(max_pages, mem_size, static_pages)| {
911                    // Last allocated page should be < `mem_size`.
912                    let end_exclusive = u32::from(mem_size) - 1;
913                    (
914                        Just(max_pages),
915                        Just(mem_size),
916                        Just(static_pages),
917                        allocations(u32::from(static_pages) as u16, end_exclusive as u16),
918                    )
919                })
920                .prop_map(
921                    |(max_pages, mem_size, static_pages, allocations)| MemoryParams {
922                        max_pages,
923                        mem_size,
924                        static_pages,
925                        allocations,
926                    },
927                )
928        }
929
930        fn proptest_config() -> ProptestConfig {
931            ProptestConfig {
932                cases: 1024,
933                ..Default::default()
934            }
935        }
936
937        #[track_caller]
938        fn assert_free_error(err: AllocError) {
939            match err {
940                AllocError::InvalidFree(_) => {}
941                AllocError::InvalidFreeRange(_, _) => {}
942                err => panic!("{err:?}"),
943            }
944        }
945
946        proptest! {
947            #![proptest_config(proptest_config())]
948            #[test]
949            fn alloc(
950                mem_params in combined_memory_params(),
951                actions in actions(),
952            ) {
953                let _ = tracing_subscriber::fmt::try_init();
954
955                let MemoryParams{max_pages, mem_size, static_pages, allocations} = mem_params;
956                let mut ctx = AllocationsContext::try_new(mem_size, allocations, static_pages, None, max_pages).unwrap();
957
958                let mut mem = TestMemory::new(mem_size);
959
960                for action in actions {
961                    match action {
962                        Action::Alloc { pages } => {
963                            match ctx.alloc::<_, NoopGrowHandler>(&mut (), &mut mem, pages, |_| Ok(())) {
964                                Err(AllocError::ProgramAllocOutOfBounds) => {
965                                    let x = mem.size(&()).add(pages);
966                                    assert!(x.is_none() || x.unwrap() > max_pages);
967                                }
968                                Ok(page) => {
969                                    assert!(pages == WasmPagesAmount::from(0) || (page >= static_pages && page < max_pages));
970                                    assert!(mem.size(&()) <= max_pages);
971                                    assert!(WasmPagesAmount::from(page).add(pages).unwrap() <= mem.size(&()));
972                                }
973                                Err(err) => panic!("{err:?}"),
974                            }
975                        }
976                        Action::Free { page } => {
977                            if let Err(err) = ctx.free(page) {
978                                assert_free_error(err);
979                            }
980                        }
981                        Action::FreeRange { page, size } => {
982                            if let Ok(interval) = Interval::<WasmPage>::with_len(page, size as u32) {
983                                let _ = ctx.free_range(interval).map_err(assert_free_error);
984                            }
985                        }
986                    }
987                }
988            }
989        }
990    }
991}