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