Title: Cute trick to mark parts of a C structure read-only
Date: 2024-11-15 13:00

Recently, I've been trying to [improve the sorry state of PHP's heap
implementation](https://github.com/php/php-src/issues/14083), small step by
small step since my free time significantly shrunk this year. Anyway,
[one of the low-hanging fruits](https://github.com/php/php-src/pull/14570) is
to makes parts of the `_zend_mm_heap` read-only, since it contains function
pointers that are often overwritten in public exploits to transform a (limited)
read/write primitive into an arbitrary code execution.

Changing memory's mode is done via `mprotect`, but it can only be done on a
per-page page-aligned granularity. The easiest way that came to mind
is to use C11's anonymous `struct` and `union` along with the `aligned`
attribute to make the structure fit neatly on pages:

```C
struct {
    union {
        struct {
            void* my_important_ptr;
            size_t my_important_size;
        };
        char padding[PAGE_SIZE];
    } ro_data
    void *my_unimportant_ptr;
    size_t my_unimportant_size;
} my_struct __attribute__((aligned(PAGE_SIZE)));

my_struct* init_my_struct() {
    assert(sizeof(my_struct) > PAGE_SIZE);
    my_struct* s = (my_struct*)malloc(sizeof(my_struct));
    if (!s) {
        return NULL;;
    }

    s->my_unimportant_ptr = NULL;
    s->my_unimportant_size = 0;
    s->my_important_ptr = NULL;
    s->my_important_size = 0;
    mprotect(s, PAGE_SIZE, PROT_READ);
    return s;
}

void set_size(my_struct* s, size_t size) {
    mprotect(s, PAGE_SIZE, PROT_WRITE);
    s->my_important_size = size;
    mprotect(s, PAGE_SIZE, PROT_READ);
}
```

Unfortunately, this isn't really portable, as `PAGE_SIZE` can't be known at
compilation-time. The recommended way to get its value is to call `long sz =
sysconf(_SC_PAGESIZE);`.

So what I went for, on the good advice of [Arnaud Le Blanc](https://mastodon.social/@arnaud_lb)
was to go the dynamic route, with two separate structures:

```C
static size_t get_page_size(void) {
	static size_t page_size = 0;
	if (!page_size) {
		page_size = sysconf(_SC_PAGESIZE);
		if (!page_size) {
			page_size = 4096; // return a sane-ish default
		}
	}
	return page_size;
}

#define GET_RO(s) ((my_struct*)((char*)(s) + get_page_size()))

struct {
    void *my_unimportant_ptr;
    size_t my_unimportant_size;
} my_struct;

struct {
    void* my_important_ptr;
    size_t my_important_size;
} ro_data

my_struct* init_my_struct() {
    assert(sizeof(my_struct) <= get_page_size());
    assert(sizeof(ro_data) <= get_page_size());

    my_struct* s = (my_struct*)malloc(get_page_size() * 2); // or posix_memalign(3)
    if (!s) {
        return NULL;;
    }

    s->my_unimportant_ptr = NULL;
    s->my_important_size = 0;
    GET_RO(s)->my_important_ptr = NULL;
    GET_RO(s)->my_important_size = 0;
    mprotect(GET_RO(s), get_page_size(), PROT_READ);
    return s;
}

void set_size(my_struct* s, size_t size) {
    mprotect(GET_RO(s), get_page_size(), PROT_WRITE);
    GET_RO(s)->my_important_size = size;
    mprotect(GET_RO(s), get_page_size(), PROT_READ);
}
```

The [pull-request](https://github.com/php/php-src/pull/14570) to implement this in php
has a bit of fluff to integrate it with PHP's memory-management lifecycle, but
should still be fairly readable. Unfortunately, it was rejected on the basis of
lowering performances by 0.6% on my local benchmark.
