Standard library · System and tools

os.mmap

Memory pages with read, write and execute permissions: map, change the permissions of and release them.

import os.mmap

Ordinary allocation (mem.Allocate) gives memory you can read and write. This package gives you whole pages straight from the operating system and lets you choose what the program may do with them: PROT_READ, PROT_WRITE and PROT_EXEC, combined with |. Alloc maps zero-filled pages, Protect changes the permissions (mprotect) and Free unmaps them. Use it for guard pages, for read-only data that must not change by accident, and for loading machine code: write the bytes while the page is writable, switch it to read and execute, then call it with os.dl.

Sizes are rounded up to whole pages by the system; PageSize tells the page size (usually 4096, 16384 on Apple silicon). The package works on Linux, macOS and the BSDs. On Windows Supported is false and Alloc returns null.

What people get wrong:

  • Asking for write and execute together. Several systems refuse it (SELinux, hardened kernels, macOS on Apple silicon, which also needs a special MAP_JIT flag this package does not set). Do it in two steps: map as PROT_READ | PROT_WRITE, fill, then Protect to PROT_READ | PROT_EXEC.
  • Passing a different size to Free or Protect than to Alloc. Use the same size; the permissions of whole pages change.
  • Using the memory after Free, or writing to a page that is now read-only. Both crash the program at once.
  • Machine code is only valid for one processor family. Code written for x86-64 will not run on an ARM computer, and some ARM systems need the instruction cache flushed after the code is written.

Running a few machine instructions

The example below maps a page, writes seven bytes of x86-64 code that add up its first two arguments, makes the page executable and calls it. It only runs on a 64 bit x86 system, which the example checks with SALAM_ARCH_X64.

os-mmap-guide-jit.salam
import os.dl
import os.mmap

func main:
    if not SALAM_ARCH_X64 or not mmap.Supported():
        println "this example needs 64 bit x86 and mmap"
        ret
    end
    page := mmap.PageSize()
    code := mmap.Alloc(page, mmap.PROT_READ | mmap.PROT_WRITE)
    bytes := code as u8*
    program := [0x48, 0x89, 0xf8, 0x48, 0x01, 0xf0, 0xc3]
    repeat 7 in i:
        bytes[i] = program[i] as u8
    end
    mmap.Protect(code, page, mmap.PROT_READ | mmap.PROT_EXEC)
    println "40 + 2 =", dl.Call(code, [40 as size, 2])
    mmap.Free(code, page)
end
Output40 + 2 = 42

A table that cannot be changed by accident

Fill a page while it is writable, then make it read-only with Protect. From then on a stray write stops the program at once instead of silently corrupting the table, and reads keep working. Make it writable again when the data has to change. The example does that with a table of squares.

os-mmap-guide-readonly.salam
import os.mmap

func main:
    if not mmap.Supported():
        println "memory mapping is not available here"
        ret
    end
    page := mmap.PageSize()
    table := mmap.Alloc(page, mmap.PROT_READ | mmap.PROT_WRITE)
    defer _freed := mmap.Free(table, page)
    cells := table as i32*
    repeat 16 in i:
        cells[i] = i * i
    end
    println "filled:", cells[3], cells[15]
    println "read-only:", mmap.Protect(table, page, mmap.PROT_READ)
    println "still readable:", cells[4]
    println "writable again:", mmap.Protect(table, page, mmap.PROT_READ | mmap.PROT_WRITE)
    cells[4] = 99
    println "after the change:", cells[4]
end
Outputfilled: 9 225 read-only: true still readable: 16 writable again: true after the change: 99

Constants and variables

PROT_NONE

const PROT_NONE: i32 = 0
Permission value: the pages may not be touched.
os-mmap-Alloc.salam
import os.mmap

func main:
    page := mmap.PageSize()
    block := mmap.Alloc(page, mmap.PROT_READ | mmap.PROT_WRITE)
    if block == null:
        println "mapping failed"
        ret
    end
    bytes := block as u8*
    bytes[0] = 7 as u8
    bytes[page - 1] = 9 as u8
    println bytes[0], bytes[1], bytes[page - 1]
    println mmap.Protect(block, page, mmap.PROT_READ)
    println bytes[0]
    println mmap.Free(block, page)
end
Output7 0 9 true 7 true

PROT_READ

const PROT_READ: i32 = 1
Permission value: the pages may be read.

The example for this item is shown with the ones above: os-mmap-Alloc.salam

PROT_WRITE

const PROT_WRITE: i32 = 2
Permission value: the pages may be written.

The example for this item is shown with the ones above: os-mmap-Alloc.salam

PROT_EXEC

const PROT_EXEC: i32 = 4
Permission value: the pages may be run as machine code.

The example for this item is shown with the ones above: os-mmap-Alloc.salam

Functions

Supported

func Supported(): bool
True where memory mapping works (every system except Windows).

The example for this item is shown with the ones above: os-mmap-Alloc.salam

PageSize

func PageSize(): int
The size of one memory page in bytes.

The example for this item is shown with the ones above: os-mmap-Alloc.salam

Alloc

func Alloc(size: int, prot: int): void*
Maps size bytes (rounded up to whole pages) of zero-filled private memory with the given permissions and returns its address, or null when it fails, when size is not positive and on Windows.

The example for this item is shown with the ones above: os-mmap-Alloc.salam

Protect

func Protect(p: void*, size: int, prot: int): bool
Changes the permissions of the pages that hold size bytes from p; true on success. p must be the address of a page, as returned by Alloc.

The example for this item is shown with the ones above: os-mmap-Alloc.salam

Free

func Free(p: void*, size: int): bool
Unmaps the pages; true on success. The memory must not be used afterwards.

The example for this item is shown with the ones above: os-mmap-Alloc.salam