mmap with PROT_READ | PROT_EXEC fails on Sonoma

I found out that this code fails on Sonoma on apple silicon:

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cassert>
#include <iostream>

int main() {
    const char* filename = "data_file";
    int dataSize = 1024;  // 1 kilobyte
    int fd;

    // Create or overwrite the file
    fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IXUSR );
    if (fd == -1) {
        perror("Error creating file");
        return 1;
    }

    // Make the file 1 KB in size
    if (ftruncate(fd, dataSize) == -1) {
        perror("Error setting file size");
        close(fd);
        return 1;
    }

    // Map the file into memory for writing
    int* writeData = (int*)mmap(NULL, dataSize, PROT_WRITE, MAP_SHARED, fd, 0);
    if (writeData == MAP_FAILED) {
        perror("Error mmaping for write");
        close(fd);
        return 1;
    }

    // Write some integer data
    for (int i = 0; i < dataSize/sizeof(int); ++i) {
        writeData[i] = i;
    }

    // Close the file and unmap memory
    if (munmap(writeData, dataSize) == -1) {
        perror("Error unmapping writeData");
    }
    close(fd);

    // Reopen the file for reading and executing
    fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("Error opening file for read|exec");
        return 1;
    }

    int* readData = (int*)mmap(NULL, dataSize, PROT_READ | PROT_EXEC, MAP_SHARED, fd, 0);
    if (readData == MAP_FAILED) {
        perror("Error mmaping for read|exec");
        close(fd);
        return 1;
    }

    // Assert the integer data is the same
    for (int i = 0; i < dataSize/sizeof(int); ++i) {
        assert(readData[i] == i);
    }
    
    std::cout << "Data verification succeeded!\n";

    // Clean up
    if (munmap(readData, dataSize) == -1) {
        perror("Error unmapping readData");
    }
    close(fd);
    unlink(filename);  // Delete the file

    return 0;
}

mmap with PROT_READ | PROT_EXEC fails with EACCESS.

and digging around the internet had led me to this commit: https://github.com/python/cpython/pull/109929/files

what was the reasoning behind this change in the API, and where is it documented? it's quite unpleasant to find changes like that in a crucial low-level calls.

Post not yet marked as solved Up vote post of byteshift Down vote post of byteshift
581 views

Replies

Can you explain more about the big picture here? That should allow me to offer specific guidance.

As to the test code you posted, I understand what it’s trying to do, but it doesn’t really make sense on Apple silicon. On Apple silicon all code must be signed [1] [2]. The test code is writing some data to a file, not signing it, and then trying to map it in as an executable (PROT_READ | PROT_EXEC). I wouldn’t expect that to work. If the mmap didn’t fail like this, you’d get a code signing crash when you touched the mapped page because there’s no hash to allow the pager to verify that the code is intact [3].

The obvious fix is to map the file back in as read-only data (PROT_READ) but that’s clearly not what you’re aiming for. And hence my question about the big picture.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Although it can be ad hoc signed.

[2] The exception to this rule is JITed code. You have to jump through a bunch of hoops to JIT on Apple silicon, but it is possible. However, that’s not what you’re doing here.

[3] See TN3126 Inside Code Signing: Hashes.

Singing part makes sense, but the main issue here is that this code worked without errors on macos 13 on m1, up until I updated it to macos 14. It also still works like that on x64 macs, and worked for decades.

My experience with binary compatibility issues like this one is that they often come down to a subtle interpretation of semantics. What are, for example, the semantics of PROT_EXEC? The man page says:

PROT_EXEC Pages may be executed.

which is tricky because, if you map unsigned data on Apple silicon then the pages can’t be executed. So, the implementation has two choices:

  • Allow the request and then fail with a very hard-to-debug code signing crash when you touch one of the mapped pages.

  • Fail the mmap call.

Personally I prefer the latter, and it seems that the engineers who made this change in macOS 14 agree. But clearly you disagree. It’s hard to comment further without knowing why the former behaviour is important to you.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"