Skip to main content

miri/shims/unix/
mem.rs

1//! This is an incomplete implementation of mmap/munmap which is restricted in order to be
2//! implementable on top of the existing memory system. The point of these function as-written is
3//! to allow memory allocators written entirely in Rust to be executed by Miri. This implementation
4//! does not support other uses of mmap such as file mappings.
5//!
6//! mmap/munmap behave a lot like alloc/dealloc, and for simple use they are exactly
7//! equivalent. That is the only part we support: no MAP_FIXED or MAP_SHARED or anything
8//! else that goes beyond a basic allocation API.
9//!
10//! Note that in addition to only supporting malloc-like calls to mmap, we only support free-like
11//! calls to munmap, but for a very different reason. In principle, according to the man pages, it
12//! is possible to unmap arbitrary regions of address space. But in a high-level language like Rust
13//! this amounts to partial deallocation, which LLVM does not support. So any attempt to call our
14//! munmap shim which would partially unmap a region of address space previously mapped by mmap will
15//! report UB.
16
17use rustc_abi::Size;
18use rustc_target::spec::Os;
19
20use crate::*;
21
22impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
23pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
24    fn mmap(
25        &mut self,
26        addr: &OpTy<'tcx>,
27        length: &OpTy<'tcx>,
28        prot: &OpTy<'tcx>,
29        flags: &OpTy<'tcx>,
30        fd: &OpTy<'tcx>,
31        offset: i128,
32    ) -> InterpResult<'tcx, Scalar> {
33        let this = self.eval_context_mut();
34
35        // We do not support MAP_FIXED, so the addr argument is always ignored (except for the MacOS hack)
36        let addr = this.read_target_usize(addr)?;
37        let length = this.read_target_usize(length)?;
38        let prot = this.read_scalar(prot)?.to_i32()?;
39        let flags = this.read_scalar(flags)?.to_i32()?;
40        let fd = this.read_scalar(fd)?.to_i32()?;
41
42        let map_private = this.eval_libc_i32("MAP_PRIVATE");
43        let map_anonymous = this.eval_libc_i32("MAP_ANONYMOUS");
44        let map_shared = this.eval_libc_i32("MAP_SHARED");
45        let map_fixed = this.eval_libc_i32("MAP_FIXED");
46
47        // This is a horrible hack, but on MacOS and Solarish the guard page mechanism uses mmap
48        // in a way we do not support. We just give it the return value it expects.
49        if this.frame_in_std()
50            && matches!(&this.tcx.sess.target.os, Os::MacOs | Os::Solaris | Os::Illumos)
51            && (flags & map_fixed) != 0
52        {
53            return interp_ok(Scalar::from_maybe_pointer(Pointer::without_provenance(addr), this));
54        }
55
56        // First, we do some basic argument validation as required by mmap
57        if (flags & (map_private | map_shared)).count_ones() != 1 {
58            this.set_last_error(LibcError("EINVAL"))?;
59            return interp_ok(this.eval_libc("MAP_FAILED"));
60        }
61        if length == 0 {
62            this.set_last_error(LibcError("EINVAL"))?;
63            return interp_ok(this.eval_libc("MAP_FAILED"));
64        }
65
66        // If a user tries to map a file, we want to loudly inform them that this is not going
67        // to work. It is possible that POSIX gives us enough leeway to return an error, but the
68        // outcome for the user (I need to add cfg(miri)) is the same, just more frustrating.
69        if fd != -1 {
70            throw_unsup_format!("Miri does not support file-backed memory mappings");
71        }
72
73        // Miri doesn't support MAP_FIXED.
74        if flags & map_fixed != 0 {
75            throw_unsup_format!(
76                "Miri does not support calls to mmap with MAP_FIXED as part of the flags argument",
77            );
78        }
79
80        verify_prot(this, prot)?;
81
82        // Miri does not support shared mappings, or any of the other extensions that for example
83        // Linux has added to the flags arguments.
84        if flags != map_private | map_anonymous {
85            throw_unsup_format!(
86                "Miri only supports calls to mmap which set the flags argument to \
87                 MAP_PRIVATE|MAP_ANONYMOUS",
88            );
89        }
90
91        // This is only used for file mappings, which we don't support anyway.
92        if offset != 0 {
93            throw_unsup_format!("Miri does not support non-zero offsets to mmap");
94        }
95
96        let align = this.machine.page_align();
97        let Some(map_length) = round_up_to_page_size(this, length) else {
98            this.set_last_error(LibcError("EINVAL"))?;
99            return interp_ok(this.eval_libc("MAP_FAILED"));
100        };
101
102        let ptr = this.allocate_ptr(
103            Size::from_bytes(map_length),
104            align,
105            MiriMemoryKind::Mmap.into(),
106            // mmap guarantees new mappings are zero-init.
107            AllocInit::Zero,
108        )?;
109
110        interp_ok(Scalar::from_pointer(ptr, this))
111    }
112
113    fn munmap(&mut self, addr: &OpTy<'tcx>, length: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
114        let this = self.eval_context_mut();
115
116        let addr = this.read_pointer(addr)?;
117        let length = this.read_target_usize(length)?;
118
119        // addr must be a multiple of the page size, but apart from that munmap is just implemented
120        // as a dealloc.
121        if !addr.addr().bytes().is_multiple_of(this.machine.page_size) {
122            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
123        }
124
125        let Some(length) = round_up_to_page_size(this, length) else {
126            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
127        };
128
129        let length = Size::from_bytes(length);
130        this.deallocate_ptr(
131            addr,
132            Some((length, this.machine.page_align())),
133            MemoryKind::Machine(MiriMemoryKind::Mmap),
134        )?;
135
136        interp_ok(Scalar::from_i32(0))
137    }
138
139    fn mprotect(
140        &mut self,
141        addr: &OpTy<'tcx>,
142        length: &OpTy<'tcx>,
143        prot: &OpTy<'tcx>,
144    ) -> InterpResult<'tcx, Scalar> {
145        let this = self.eval_context_mut();
146
147        let addr = this.read_pointer(addr)?;
148        let length = this.read_target_usize(length)?;
149        let prot = this.read_scalar(prot)?.to_i32()?;
150
151        // addr must be a multiple of the page size.
152        if !addr.addr().bytes().is_multiple_of(this.machine.page_size) {
153            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
154        }
155
156        verify_prot(this, prot)?;
157
158        // The pages from `[addr, addr + length)` must be mapped, so length definitely must not overflow.
159        let Some(length) = round_up_to_page_size(this, length) else {
160            return this.set_errno_and_return_neg1_i32(LibcError("ENOMEM"));
161        };
162        // Ensure this is actually allocated memory we can access.
163        this.check_ptr_access(addr, Size::from_bytes(length), CheckInAllocMsg::MemoryAccess)
164            .map_err_kind(|_| err_ub_format!("`mprotect` called on out-of-bounds memory"))?;
165
166        // If the memory comes from memory the Rust program has allocated with mmap, we only support
167        // `PROT_READ|PROT_WRITE`, so this `mprotect` is a no-op. If the memory was mmaped by the
168        // runtime (e.g. if it's the stack, executable memory, or static memory), POSIX also allows
169        // us to remap it. In those cases, such a call to `PROT_READ|PROT_WRITE` might actually change the permissions,
170        // but treating them as the new permissions is still UB. Therefore, we just pretend that we
171        // did the permission change by returning success, and will still reject if you try to use
172        // it with the "new" permissions.
173        interp_ok(Scalar::from_i32(0))
174    }
175
176    fn madvise(
177        &mut self,
178        addr: &OpTy<'tcx>,
179        length: &OpTy<'tcx>,
180        advice: &OpTy<'tcx>,
181    ) -> InterpResult<'tcx, Scalar> {
182        let this = self.eval_context_mut();
183
184        let addr = this.read_pointer(addr)?;
185        let length = this.read_target_usize(length)?;
186        let advise = this.read_scalar(advice)?.to_i32()?;
187
188        // addr must be a multiple of the page size.
189        if !addr.addr().bytes().is_multiple_of(this.machine.page_size) {
190            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
191        }
192
193        // advise must be supported.
194        let madv_normal = this.eval_libc_i32("MADV_NORMAL");
195        let madv_random = this.eval_libc_i32("MADV_RANDOM");
196        let madv_sequential = this.eval_libc_i32("MADV_SEQUENTIAL");
197        let madv_willneed = this.eval_libc_i32("MADV_WILLNEED");
198        if advise != madv_normal
199            && advise != madv_random
200            && advise != madv_sequential
201            && advise != madv_willneed
202        {
203            throw_unsup_format!(
204                "Miri does not support calls to madvise with advice other than MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL, or MADV_WILLNEED",
205            );
206        }
207
208        // The pages from `[addr, addr + length)` must be mapped, so length definitely must not overflow.
209        let Some(length) = round_up_to_page_size(this, length) else {
210            return this.set_errno_and_return_neg1_i32(LibcError("ENOMEM"));
211        };
212        // Ensure this is actually allocated memory we can access.
213        this.check_ptr_access(addr, Size::from_bytes(length), CheckInAllocMsg::MemoryAccess)
214            .map_err_kind(|_| err_ub_format!("`madvise` called on out-of-bounds memory"))?;
215
216        // All advises we support are no-ops.
217        interp_ok(Scalar::from_i32(0))
218    }
219}
220
221fn round_up_to_page_size(this: &MiriInterpCx<'_>, length: u64) -> Option<u64> {
222    length
223        .checked_next_multiple_of(this.machine.page_size)
224        .filter(|length| *length <= this.target_isize_max().try_into().unwrap())
225}
226
227fn verify_prot<'tcx>(this: &mut MiriInterpCx<'tcx>, prot: i32) -> InterpResult<'tcx> {
228    let prot_read = this.eval_libc_i32("PROT_READ");
229    let prot_write = this.eval_libc_i32("PROT_WRITE");
230
231    // Miri doesn't support protections other than PROT_READ|PROT_WRITE.
232    if prot != prot_read | prot_write {
233        throw_unsup_format!(
234            "Miri does not support calls to mmap/mprotect with protections other than \
235             PROT_READ|PROT_WRITE",
236        );
237    }
238
239    interp_ok(())
240}