Skip to content

Safeguards / alternatives for MAP_FIXED mmap? #73

Description

@rakslice

Background:

On a real 68k host, where the Mac memory addresses need to be real host program memory addresses, and for emulation speed on non-68k hosts, macemu can take advantage of having the mac memory in host memory at a fixed location known at compile time, so that the offset between mac addresses and host addresses is fixed. The general case of a fixed offset is DIRECT_ADDRESSING; the special case where that offset is 0 is REAL_ADDRESSING. For more information on addressing modes see TECH. These fixed offsets are especially useful under JIT where it means you don't have to deal with shuffling a memory offset around, and reserve another register or incur access to another global to get it basically every load/store.

Of course this is used for the mac's entire main RAM and ROM, which we know the sizes of at launch time.

But also the framebuffer: The various SDL video implementations basically assume you are doing one of the fixed addressing modes, and always use their vm_acquire_framebuffer() to do a fixed allocation for the framebuffer, using whatever suitable OS platform functionality exists in vm_alloc.cpp, and the way the code is organized it needs to free and reallocate that framebuffer as the size changes when we change the video mode.

To support a fixed address block on a modern OS with virtual addressing you need to allocate process memory at fixed addresses.

On a platform where there is mmap() that supports the MAP_FIXED flag we use that...

The issue:

Unfortunately the common mmap() MAP_FIXED behaviour is that if there is already a memory allocation in the process address space covering the memory address range you asked for, it just cuts it up and deletes the part that overlaps the request and gives it to the mmap() caller instead.

And with multiple threads potentially making allocations you can't just use the API to check there are no pages currently in the range and then allocate.

Let me just quote the Linux manpage verbatim:

       MAP_FIXED
              Don't interpret addr as a hint: place the mapping at
              exactly that address.  addr must be suitably aligned: for
              most architectures a multiple of the page size is
              sufficient; however, some architectures may impose
              additional restrictions.  If the memory region specified
              by addr and length overlaps pages of any existing
              mapping(s), then the overlapped part of the existing
              mapping(s) will be discarded.  If the specified address
              cannot be used, mmap() will fail.

              Software that aspires to be portable should use the
              MAP_FIXED flag with care, keeping in mind that the exact
              layout of a process's memory mappings is allowed to change
              significantly between Linux versions, C library versions,
              and operating system releases.  Carefully read the
              discussion of this flag in NOTES!

[From NOTES:]

Using MAP_FIXED safely
       The only safe use for MAP_FIXED is where the address range
       specified by addr and length was previously reserved using
       another mapping; otherwise, the use of MAP_FIXED is hazardous
       because it forcibly removes preexisting mappings, making it easy
       for a multithreaded process to corrupt its own address space.

       For example, suppose that thread A looks through /proc/pid/maps
       in order to locate an unused address range that it can map using
       MAP_FIXED, while thread B simultaneously acquires part or all of
       that same address range.  When thread A subsequently employs
       mmap(MAP_FIXED), it will effectively clobber the mapping that
       thread B created.  In this scenario, thread B need not create a
       mapping directly; simply making a library call that, internally,
       uses [dlopen(3)](https://man7.org/linux/man-pages/man3/dlopen.3.html) to load some other shared library, will suffice.
       The [dlopen(3)](https://man7.org/linux/man-pages/man3/dlopen.3.html) call will map the library into the process's
       address space.  Furthermore, almost any library call may be
       implemented in a way that adds memory mappings to the address
       space, either with this technique, or by simply allocating
       memory.  Examples include [brk(2)](https://man7.org/linux/man-pages/man2/brk.2.html), [malloc(3)](https://man7.org/linux/man-pages/man3/malloc.3.html), [pthread_create(3)](https://man7.org/linux/man-pages/man3/pthread_create.3.html),
       and the PAM libraries ⟨http://www.linux-pam.org/⟩.

       Since Linux 4.17, a multithreaded program can use the
       MAP_FIXED_NOREPLACE flag to avoid the hazard described above when
       attempting to create a mapping at a fixed address that has not
       been reserved by a preexisting mapping.

Here's FreeBSD:

       MAP_FIXED	  Do not permit	the system to select a	different  ad-
			  dress	 than the one specified.  If the specified ad-
			  dress	 cannot	 be  used,  mmap()  will   fail.    If
			  MAP_FIXED  is	 specified, addr must be a multiple of
			  the page size.  If MAP_EXCL is not specified,	a suc-
			  cessful MAP_FIXED request replaces any previous map-
			  pings	for the	process' pages in the range from  addr
			  to  addr  + len.  In contrast, if MAP_EXCL is	speci-
			  fied,	the request will fail if a mapping already ex-
			  ists within the range.

OpenBSD:

If the MAP_FIXED flag is specified, the allocation will happen at the specified address, 
replacing any previously established mappings in its range. 

Solutions:

There's no way to actually recover if the fixed memory allocation is actually needed and the memory space in the process is already allocated; the only possible improvement to a given fixed allocation call as it concerns memory already allocated is that we detect that it can't not overlap and crash out and at least not give the user a partly working system that is slowly destroying itself. But of course, if we can do the fixed allocations more wisely in the first place, i.e. 1) do them once and never change them and 2) do them as soon as possible at program start so the least possible other stuff is allocated, that would be an improvement irrespective of whether we can detect the overlap or not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions