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    scale::{self, Decode, Encode, EncodeLike, Input, Output},
39    TypeInfo,
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, 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, 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            if stack_end > static_pages {
383                return Err(MemorySetupError::StackEndOutOfStaticMemory {
384                    stack_end,
385                    static_pages,
386                });
387            }
388        }
389
390        if let Some(page) = allocations.end() {
391            if page >= memory_size {
392                return Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
393                    page,
394                    static_pages,
395                    memory_size,
396                });
397            }
398        }
399        if let Some(page) = allocations.start() {
400            if page < static_pages {
401                return Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
402                    page,
403                    static_pages,
404                    memory_size,
405                });
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            if page >= heap.start() && page <= heap.end() && self.allocations.remove(page) {
465                self.allocations_changed = true;
466                return Ok(());
467            }
468        }
469        Err(AllocError::InvalidFree(page))
470    }
471
472    /// Try to free pages in range. Will only return error if range is invalid.
473    ///
474    /// Currently running program should own this pages.
475    pub fn free_range(&mut self, interval: Interval<WasmPage>) -> Result<(), AllocError> {
476        if let Some(heap) = self.heap {
477            // `free_range` allows do not modify the allocations so we do not check the `remove` result here
478            if interval.start() >= heap.start() && interval.end() <= heap.end() {
479                if self.allocations.remove(interval) {
480                    self.allocations_changed = true;
481                }
482
483                return Ok(());
484            }
485        }
486
487        Err(AllocError::InvalidFreeRange(
488            interval.start(),
489            interval.end(),
490        ))
491    }
492
493    /// Decomposes this instance and returns `static_pages`, `allocations` and `allocations_changed` params.
494    pub fn into_parts(self) -> (WasmPagesAmount, IntervalsTree<WasmPage>, bool) {
495        (
496            self.static_pages,
497            self.allocations,
498            self.allocations_changed,
499        )
500    }
501}
502
503/// This module contains tests of `GearPage` and `AllocationContext`
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use alloc::vec::Vec;
508    use core::{cell::Cell, iter};
509
510    struct TestMemory(Cell<WasmPagesAmount>);
511
512    impl TestMemory {
513        fn new(amount: WasmPagesAmount) -> Self {
514            Self(Cell::new(amount))
515        }
516    }
517
518    impl Memory<()> for TestMemory {
519        type GrowError = ();
520
521        fn grow(&self, _ctx: &mut (), pages: WasmPagesAmount) -> Result<(), Self::GrowError> {
522            let new_pages_amount = self.0.get().add(pages).ok_or(())?;
523            self.0.set(new_pages_amount);
524            Ok(())
525        }
526
527        fn size(&self, _ctx: &()) -> WasmPagesAmount {
528            self.0.get()
529        }
530
531        fn write(&self, _ctx: &mut (), _offset: u32, _buffer: &[u8]) -> Result<(), MemoryError> {
532            unimplemented!()
533        }
534
535        fn read(&self, _ctx: &(), _offset: u32, _buffer: &mut [u8]) -> Result<(), MemoryError> {
536            unimplemented!()
537        }
538
539        unsafe fn get_buffer_host_addr_unsafe(&self, _ctx: &()) -> HostPointer {
540            unimplemented!()
541        }
542    }
543
544    #[test]
545    fn page_buf() {
546        let _ = tracing_subscriber::fmt::try_init();
547
548        let mut data = PageBufInner::filled_with(199u8);
549        data.inner_mut()[1] = 2;
550        let page_buf = PageBuf::from_inner(data);
551        log::debug!("page buff = {:?}", page_buf);
552    }
553
554    #[test]
555    fn free_fails() {
556        let mut ctx =
557            AllocationsContext::try_new(0.into(), Default::default(), 0.into(), None, 0.into())
558                .unwrap();
559        assert_eq!(ctx.free(1.into()), Err(AllocError::InvalidFree(1.into())));
560
561        let mut ctx = AllocationsContext::try_new(
562            1.into(),
563            [WasmPage::from(0)].into_iter().collect(),
564            0.into(),
565            None,
566            1.into(),
567        )
568        .unwrap();
569        assert_eq!(ctx.free(1.into()), Err(AllocError::InvalidFree(1.into())));
570
571        let mut ctx = AllocationsContext::try_new(
572            4.into(),
573            [WasmPage::from(1), WasmPage::from(3)].into_iter().collect(),
574            1.into(),
575            None,
576            4.into(),
577        )
578        .unwrap();
579        let interval = Interval::<WasmPage>::try_from(1u16..4).unwrap();
580        assert_eq!(ctx.free_range(interval), Ok(()));
581    }
582
583    #[track_caller]
584    fn alloc_ok(ctx: &mut AllocationsContext, mem: &mut TestMemory, pages: u16, expected: u16) {
585        let res = ctx.alloc::<(), NoopGrowHandler>(&mut (), mem, pages.into(), |_| Ok(()));
586        assert_eq!(res, Ok(expected.into()));
587    }
588
589    #[track_caller]
590    fn alloc_err(ctx: &mut AllocationsContext, mem: &mut TestMemory, pages: u16, err: AllocError) {
591        let res = ctx.alloc::<(), NoopGrowHandler>(&mut (), mem, pages.into(), |_| Ok(()));
592        assert_eq!(res, Err(err));
593    }
594
595    #[test]
596    fn alloc() {
597        let _ = tracing_subscriber::fmt::try_init();
598
599        let mut ctx = AllocationsContext::try_new(
600            256.into(),
601            Default::default(),
602            16.into(),
603            None,
604            256.into(),
605        )
606        .unwrap();
607        let mut mem = TestMemory::new(16.into());
608        alloc_ok(&mut ctx, &mut mem, 16, 16);
609        alloc_ok(&mut ctx, &mut mem, 0, 16);
610
611        // there is a space for 14 more
612        (2..16).for_each(|i| alloc_ok(&mut ctx, &mut mem, 16, i * 16));
613
614        // no more mem!
615        alloc_err(&mut ctx, &mut mem, 16, AllocError::ProgramAllocOutOfBounds);
616
617        // but we free some and then can allocate page that was freed
618        ctx.free(137.into()).unwrap();
619        alloc_ok(&mut ctx, &mut mem, 1, 137);
620
621        // if we free 2 in a row we can allocate even 2
622        ctx.free(117.into()).unwrap();
623        ctx.free(118.into()).unwrap();
624        alloc_ok(&mut ctx, &mut mem, 2, 117);
625
626        // same as above, if we free_range 2 in a row we can allocate 2
627        let interval = Interval::<WasmPage>::try_from(117..119).unwrap();
628        ctx.free_range(interval).unwrap();
629        alloc_ok(&mut ctx, &mut mem, 2, 117);
630
631        // but if 2 are not in a row, bad luck
632        ctx.free(117.into()).unwrap();
633        ctx.free(158.into()).unwrap();
634        alloc_err(&mut ctx, &mut mem, 2, AllocError::ProgramAllocOutOfBounds);
635    }
636
637    #[test]
638    fn memory_params_validation() {
639        assert_eq!(
640            AllocationsContext::validate_memory_params(
641                4.into(),
642                &iter::once(WasmPage::from(2)).collect(),
643                2.into(),
644                Some(2.into()),
645                4.into(),
646            ),
647            Ok(())
648        );
649
650        assert_eq!(
651            AllocationsContext::validate_memory_params(
652                4.into(),
653                &Default::default(),
654                2.into(),
655                Some(2.into()),
656                3.into(),
657            ),
658            Err(MemorySetupError::MemorySizeExceedsMaxPages {
659                memory_size: 4.into(),
660                max_pages: 3.into()
661            })
662        );
663
664        assert_eq!(
665            AllocationsContext::validate_memory_params(
666                1.into(),
667                &Default::default(),
668                2.into(),
669                Some(1.into()),
670                4.into(),
671            ),
672            Err(MemorySetupError::InsufficientMemorySize {
673                memory_size: 1.into(),
674                static_pages: 2.into()
675            })
676        );
677
678        assert_eq!(
679            AllocationsContext::validate_memory_params(
680                4.into(),
681                &Default::default(),
682                2.into(),
683                Some(3.into()),
684                4.into(),
685            ),
686            Err(MemorySetupError::StackEndOutOfStaticMemory {
687                stack_end: 3.into(),
688                static_pages: 2.into()
689            })
690        );
691
692        assert_eq!(
693            AllocationsContext::validate_memory_params(
694                4.into(),
695                &[WasmPage::from(1), WasmPage::from(3)].into_iter().collect(),
696                2.into(),
697                Some(2.into()),
698                4.into(),
699            ),
700            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
701                page: 1.into(),
702                static_pages: 2.into(),
703                memory_size: 4.into()
704            })
705        );
706
707        assert_eq!(
708            AllocationsContext::validate_memory_params(
709                4.into(),
710                &[WasmPage::from(2), WasmPage::from(4)].into_iter().collect(),
711                2.into(),
712                Some(2.into()),
713                4.into(),
714            ),
715            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
716                page: 4.into(),
717                static_pages: 2.into(),
718                memory_size: 4.into()
719            })
720        );
721
722        assert_eq!(
723            AllocationsContext::validate_memory_params(
724                13.into(),
725                &iter::once(WasmPage::from(1)).collect(),
726                10.into(),
727                None,
728                13.into()
729            ),
730            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
731                page: 1.into(),
732                static_pages: 10.into(),
733                memory_size: 13.into()
734            })
735        );
736
737        assert_eq!(
738            AllocationsContext::validate_memory_params(
739                13.into(),
740                &iter::once(WasmPage::from(1)).collect(),
741                WasmPagesAmount::UPPER,
742                None,
743                13.into()
744            ),
745            Err(MemorySetupError::InsufficientMemorySize {
746                memory_size: 13.into(),
747                static_pages: WasmPagesAmount::UPPER
748            })
749        );
750
751        assert_eq!(
752            AllocationsContext::validate_memory_params(
753                WasmPagesAmount::UPPER,
754                &iter::once(WasmPage::from(1)).collect(),
755                10.into(),
756                None,
757                WasmPagesAmount::UPPER,
758            ),
759            Err(MemorySetupError::AllocatedPageOutOfAllowedInterval {
760                page: 1.into(),
761                static_pages: 10.into(),
762                memory_size: WasmPagesAmount::UPPER
763            })
764        );
765    }
766
767    #[test]
768    fn allocations_changed_correctness() {
769        let new_ctx = |allocations| {
770            AllocationsContext::try_new(16.into(), allocations, 0.into(), None, 16.into()).unwrap()
771        };
772
773        // correct `alloc`
774        let mut ctx = new_ctx(Default::default());
775        assert!(
776            !ctx.allocations_changed,
777            "Expecting no changes after creation"
778        );
779        let mut mem = TestMemory::new(16.into());
780        alloc_ok(&mut ctx, &mut mem, 16, 0);
781        assert!(ctx.allocations_changed);
782
783        let (_, allocations, allocations_changed) = ctx.into_parts();
784        assert!(allocations_changed);
785
786        // fail `alloc`
787        let mut ctx = new_ctx(allocations);
788        alloc_err(&mut ctx, &mut mem, 16, AllocError::ProgramAllocOutOfBounds);
789        assert!(
790            !ctx.allocations_changed,
791            "Expecting allocations don't change because of error"
792        );
793
794        // fail `free`
795        assert!(ctx.free(16.into()).is_err());
796        assert!(!ctx.allocations_changed);
797
798        // correct `free`
799        assert!(ctx.free(10.into()).is_ok());
800        assert!(ctx.allocations_changed);
801
802        let (_, allocations, allocations_changed) = ctx.into_parts();
803        assert!(allocations_changed);
804
805        // correct `free_range`
806        // allocations: [0..9] ∪ [11..15]
807        let mut ctx = new_ctx(allocations);
808        let interval = Interval::<WasmPage>::try_from(10u16..12).unwrap();
809        assert!(ctx.free_range(interval).is_ok());
810        assert!(
811            ctx.allocations_changed,
812            "Expected value is `true` because the 11th page was freed from allocations."
813        );
814
815        let (_, allocations, allocations_changed) = ctx.into_parts();
816        assert!(allocations_changed);
817
818        // fail `free_range`
819        let mut ctx = new_ctx(allocations);
820        let interval = Interval::<WasmPage>::try_from(0u16..17).unwrap();
821        assert!(ctx.free_range(interval).is_err());
822        assert!(!ctx.allocations_changed);
823        assert!(!ctx.into_parts().2);
824    }
825
826    mod property_tests {
827        use super::*;
828        use proptest::{
829            arbitrary::any,
830            collection::size_range,
831            prop_oneof, proptest,
832            strategy::{Just, Strategy},
833            test_runner::Config as ProptestConfig,
834        };
835
836        #[derive(Debug, Clone)]
837        enum Action {
838            Alloc { pages: WasmPagesAmount },
839            Free { page: WasmPage },
840            FreeRange { page: WasmPage, size: u8 },
841        }
842
843        fn actions() -> impl Strategy<Value = Vec<Action>> {
844            let action = prop_oneof![
845                // Allocate smaller number (0..32) of pages due to `BTree::extend` significantly slows down prop-test.
846                wasm_pages_amount_with_range(0, 32).prop_map(|pages| Action::Alloc { pages }),
847                wasm_page().prop_map(|page| Action::Free { page }),
848                (wasm_page(), any::<u8>())
849                    .prop_map(|(page, size)| Action::FreeRange { page, size }),
850            ];
851            proptest::collection::vec(action, 0..1024)
852        }
853
854        fn allocations(start: u16, end: u16) -> impl Strategy<Value = IntervalsTree<WasmPage>> {
855            proptest::collection::btree_set(wasm_page_with_range(start, end), size_range(0..1024))
856                .prop_map(|pages| pages.into_iter().collect::<IntervalsTree<WasmPage>>())
857        }
858
859        fn wasm_page_with_range(start: u16, end: u16) -> impl Strategy<Value = WasmPage> {
860            (start..=end).prop_map(WasmPage::from)
861        }
862
863        fn wasm_page() -> impl Strategy<Value = WasmPage> {
864            wasm_page_with_range(0, u16::MAX)
865        }
866
867        fn wasm_pages_amount_with_range(
868            start: u32,
869            end: u32,
870        ) -> impl Strategy<Value = WasmPagesAmount> {
871            (start..=end).prop_map(|x| {
872                if x == u16::MAX as u32 + 1 {
873                    WasmPagesAmount::UPPER
874                } else {
875                    WasmPagesAmount::from(x as u16)
876                }
877            })
878        }
879
880        fn wasm_pages_amount() -> impl Strategy<Value = WasmPagesAmount> {
881            wasm_pages_amount_with_range(0, u16::MAX as u32 + 1)
882        }
883
884        #[derive(Debug)]
885        struct MemoryParams {
886            max_pages: WasmPagesAmount,
887            mem_size: WasmPagesAmount,
888            static_pages: WasmPagesAmount,
889            allocations: IntervalsTree<WasmPage>,
890        }
891
892        // This high-order strategy generates valid memory parameters in a specific way that allows passing `AllocationContext::validate_memory_params` checks.
893        fn combined_memory_params() -> impl Strategy<Value = MemoryParams> {
894            wasm_pages_amount()
895                .prop_flat_map(|max_pages| {
896                    let mem_size = wasm_pages_amount_with_range(0, u32::from(max_pages));
897                    (Just(max_pages), mem_size)
898                })
899                .prop_flat_map(|(max_pages, mem_size)| {
900                    let static_pages = wasm_pages_amount_with_range(0, u32::from(mem_size));
901                    (Just(max_pages), Just(mem_size), static_pages)
902                })
903                .prop_filter(
904                    "filter out cases where allocation region has zero size",
905                    |(_max_pages, mem_size, static_pages)| static_pages < mem_size,
906                )
907                .prop_flat_map(|(max_pages, mem_size, static_pages)| {
908                    // Last allocated page should be < `mem_size`.
909                    let end_exclusive = u32::from(mem_size) - 1;
910                    (
911                        Just(max_pages),
912                        Just(mem_size),
913                        Just(static_pages),
914                        allocations(u32::from(static_pages) as u16, end_exclusive as u16),
915                    )
916                })
917                .prop_map(
918                    |(max_pages, mem_size, static_pages, allocations)| MemoryParams {
919                        max_pages,
920                        mem_size,
921                        static_pages,
922                        allocations,
923                    },
924                )
925        }
926
927        fn proptest_config() -> ProptestConfig {
928            ProptestConfig {
929                cases: 1024,
930                ..Default::default()
931            }
932        }
933
934        #[track_caller]
935        fn assert_free_error(err: AllocError) {
936            match err {
937                AllocError::InvalidFree(_) => {}
938                AllocError::InvalidFreeRange(_, _) => {}
939                err => panic!("{err:?}"),
940            }
941        }
942
943        proptest! {
944            #![proptest_config(proptest_config())]
945            #[test]
946            fn alloc(
947                mem_params in combined_memory_params(),
948                actions in actions(),
949            ) {
950                let _ = tracing_subscriber::fmt::try_init();
951
952                let MemoryParams{max_pages, mem_size, static_pages, allocations} = mem_params;
953                let mut ctx = AllocationsContext::try_new(mem_size, allocations, static_pages, None, max_pages).unwrap();
954
955                let mut mem = TestMemory::new(mem_size);
956
957                for action in actions {
958                    match action {
959                        Action::Alloc { pages } => {
960                            match ctx.alloc::<_, NoopGrowHandler>(&mut (), &mut mem, pages, |_| Ok(())) {
961                                Err(AllocError::ProgramAllocOutOfBounds) => {
962                                    let x = mem.size(&()).add(pages);
963                                    assert!(x.is_none() || x.unwrap() > max_pages);
964                                }
965                                Ok(page) => {
966                                    assert!(pages == WasmPagesAmount::from(0) || (page >= static_pages && page < max_pages));
967                                    assert!(mem.size(&()) <= max_pages);
968                                    assert!(WasmPagesAmount::from(page).add(pages).unwrap() <= mem.size(&()));
969                                }
970                                Err(err) => panic!("{err:?}"),
971                            }
972                        }
973                        Action::Free { page } => {
974                            if let Err(err) = ctx.free(page) {
975                                assert_free_error(err);
976                            }
977                        }
978                        Action::FreeRange { page, size } => {
979                            if let Ok(interval) = Interval::<WasmPage>::with_len(page, size as u32) {
980                                let _ = ctx.free_range(interval).map_err(assert_free_error);
981                            }
982                        }
983                    }
984                }
985            }
986        }
987    }
988}