Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Remote heap feng shui / heap spraying protection #14304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
arnaud-lb wants to merge 8 commits into php:master
base: master
Choose a base branch
Loading
from arnaud-lb:mm-zones

Conversation

Copy link
Member

@arnaud-lb arnaud-lb commented May 23, 2024
edited
Loading

This isolates remotely-controlled allocations (user input / request environment) in separate chunks, to make it more difficult for a remote attacker to precisely control the layout of the heap before the application starts, or to spray the heap with specific content. The general idea is similar to https://lwn.net/Articles/965837/ (not necessarily the implementation).

Design:

The heap is split in two zones, each with its own set of freelists (free_slot) and chunks so that allocations in a zone do not impact the layout of the other one. There is a notion of current zone, from which all allocations are made. The current zone is switched to the input zone before handling a request, and switched back to the default zone after input handling.

A new field heap.zone_free_slot points to the freelist of the current zone. The only change required in allocation operations is to refer to that instead of heap.free_slot. Similarly, in free operations we find the zone_free_slot in the chunk.

realloc() allocates new blocks in the current zone (if truncation or extension is required), regardless of the original zone in which the block was allocated, so that it's not possible to profit of the layout of the original zone.

It is valid to switch zones at any time, so we can switch zones before/after handling JIT super globals, for instance.

Future scope would be to activate the input zone in more places (e.g. when parsing json, during unserialize, etc), or to create more zones for various purposes.

Time overhead is very small (~+0% wall time, +0.13% valgrind).

The minimal memory usage is now 4MiB (two chunks) instead of 2MiB, but this does not necessarily translates to that much increase in RSS.

Related: #14083

nielsdos, KennedyTedesco, and jvoisin reacted with thumbs up emoji

#if ZEND_MM_HEAP_SPRAYING_PROTECTION

# define ZEND_MM_ZONES 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# define ZEND_MM_ZONES 2
# define ZEND_MM_ZONES 2/* one zone for trusted data, one for user-controlled ones.*/

Copy link
Member Author

@arnaud-lb arnaud-lb May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitating on this one, as I feel that how zones are used belongs to upper abstraction levels (that's also why I've defined ZEND_MM_ZONE_INPUT elsewhere). In any case I agree that a comment would help.

Copy link
Contributor

@jvoisin jvoisin Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, but I also agree that a comment would be nice :)

arnaud-lb reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2M * 100 workers = waster of 200MB

Copy link
Member Author

@arnaud-lb arnaud-lb Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This consumes an additional 2MiB of VMA per worker, but I expect the overhead of actually committed memory to be much less than that in practice, as not all pages of the extra chunk are touched.

For example, at the end of a symfony-demo request, we have this:

base: VmRSS:	 65772 kB
mm-zones: VmRSS:	 66020 kB
Diff: 249 kB

With USE_ZEND_ALLOC_HUGE_PAGES=1, the RSS actually increases by 2MiB, but this is something to expect with huge pages, and this is not enabled by default.

@arnaud-lb arnaud-lb changed the title (削除) Remote heap feng chui / heap spraying protection (削除ここまで) (追記) Remote heap feng shui / heap spraying protection (追記ここまで) May 29, 2024
Copy link
Contributor

jvoisin commented Jul 10, 2024

Now that #14054 was merged, can you please rebase this one, so we can get it landed?

Copy link
Contributor

jvoisin commented Nov 4, 2024

@arnaud-lb ping :)

Copy link
Member Author

Sorry I didn't see your previous comment. I will get back at this PR soon.

Copy link
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this lands, please add a note to UPGRADING in other changes about the increase in memory requirements.

arnaud-lb reacted with thumbs up emoji
Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the first look, I don't like this. What kind of attacks will this complicate?


#if ZEND_MM_HEAP_SPRAYING_PROTECTION

# define ZEND_MM_ZONES 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2M * 100 workers = waster of 200MB

Comment on lines -1435 to +1547
zend_mm_set_next_free_slot(heap, bin_num, p, heap->free_slot[bin_num]);
heap->free_slot[bin_num] = p;
zend_mm_set_next_free_slot(heap, bin_num, p, ZEND_MM_FREE_SLOT_EX(heap, chunk, bin_num));
ZEND_MM_FREE_SLOT_EX(heap, chunk, bin_num) = p;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This make regression for each deallocation.

Copy link
Member Author

@arnaud-lb arnaud-lb Nov 11, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds an additional fetch of chunk->zone_free_slot, but this field is just next to chunk->heap, and is likely in cache as we fetch it here:

php-src/Zend/zend_alloc.c

Lines 2701 to 2702 in 53df3ae

ZEND_MM_CHECK(chunk->heap == AG(mm_heap), "zend_mm_heap corrupted"); \
zend_mm_free_small(AG(mm_heap), ptr, _num); \

Also, benchmark results show very little overhead (0% wall time, 0.04% valgrind)

Copy link
Member Author

arnaud-lb commented Nov 11, 2024
edited
Loading

@dstogov

What kind of attacks will this complicate?

An attacker can precisely control the layout of the heap by sending GET or POST parameters. For example, the query string ?a=aaa...&b=bbb...&c=ccc...&b= allocates 4 blocks of arbitrary size, frees the second one, and puts it at the top of the freelist. So the attacker is able to precisely control what is allocated where during application execution, and what is next to it in memory. This greatly facilitates the exploitation of all kinds of memory safety issues. See https://www.youtube.com/watch?v=-FXvUe0tySM&t=1233s (starting at 18:28) for example.

By isolating the user inputs in separate chunks, we prevent grooming the application heap via GET/POST/etc. This is useful in the context of a remote attacker with no ability to execute arbitrary code.

As the overhead is very small (0% wall time / 0.04% valgrind on the symfony-demo benchmark), I believe this is worth it.

Copy link
Member

dstogov commented Nov 11, 2024

@arnaud-lb didn't you already add the protection against heap buffer overflow/underflow through the shadow pointers?
I thought, it shouldn't be possible to corrupt the free-list now.
why do we need this patch on top of shadow pointers?

do you have the source of the exploit used in the presentation?

Personally, I think that the better approach would be filtering input data and just stopping the request in case of unexpected/dangerous data detection (e.g. GET/POST with duplicate names).

Copy link
Member Author

@dstogov

didn't you already add the protection against heap buffer overflow/underflow through the shadow pointers?

Yes, but that's not the only way to exploit an out of bound write. If an attacker can arrange the heap to override the first bytes of an arbitrary block, there are other things they can attack. Protecting against freelist corruption specifically was worth it because it's an easy and powerful target, but there are other targets that are made practicable by arranging the layout of the heap.

do you have the source of the exploit used in the presentation?

I've tried to find it, but without success

Personally, I think that the better approach would be filtering input data and just stopping the request in case of unexpected/dangerous data detection (e.g. GET/POST with duplicate names).

This will prevent controlling the order of elements in the freelist, but this is not absolutely necessary to control block placement in the heap. For instance, an attacker can control the order of runs in a chunk (without unsetting a GET/POST element), as well as how much they are filled, so they can arrange for a block of size N allocated by the application to be at the end of a run, and just before an other run of their choice.

Copy link
Member

dstogov commented Nov 11, 2024

do you have the source of the exploit used in the presentation?

I've tried to find it, but without success

@cfreal could you please share the sources of exploit used in your presentation? (better to email to my and @arnaud-lb public github email addresses)

Copy link
Contributor

jvoisin commented Nov 11, 2024
edited
Loading

Similar exploits are available here, with a detailed write-up about the heap shaping here

Copy link

cfreal commented Nov 11, 2024

What @jvoisin said. Relevant section here. I can provide the adminer exploit as well tomorrow if you find it useful.

Copy link
Member

dstogov commented Nov 12, 2024

@jvoisin @cfreal thank you very much. I'll need to play with this.

Copy link

m4p1e commented Nov 18, 2024
edited
Loading

Future scope would be to activate the input zone in more places (e.g. when parsing json, during unserialize, etc), or to create more zones for various purposes.

Also, do not abuse the userinput zone, as it may introduce a new attack surface. For example, if there is a potential vulnerability in the unserialize process and unserialize-related operations are added to the userinput zone, an attacker could use an HTTP request to arrange an ideal memory layout without needing to account for PHP internals.

arnaud-lb reacted with thumbs up emoji

Copy link
Contributor

jvoisin commented Nov 18, 2024

Also, do not abuse the userinput zone, as it may introduce a new attack surface. For example, if there is a potential vulnerability in the unserialize process and unserialize-related operations are added to the userinput zone, an attacker could use an HTTP request to arrange an ideal memory layout without needing to account for PHP internals.

Unserialized is already providing enough control to an attacker, this wouldn't change much :D But more seriously, partitioning memory by types (whether primitive types/size, or usage-types) is part of #14083's roadmap :)

arnaud-lb reacted with thumbs up emoji

Copy link
Contributor

jvoisin commented Apr 8, 2025

@dstogov did you have time to take a look at this?

Copy link
Member

dstogov commented Apr 14, 2025

@arnaud-lb please discuss this with @nielsdos, @iluuu1994 and take the decision.
I don't like this over-complication, but I also don't like to be a blocker.

Copy link
Contributor

jvoisin commented Jul 29, 2025

Hey @nielsdos and @iluuu1994, did you have time to look at this?

Copy link
Member

No I didn't look at this yet in detail.
I need to make time for this in the weekend.

jvoisin reacted with heart emoji

Copy link
Member

@nielsdos nielsdos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite see where the initial call for zend_mm_userinput_begin is?

Copy link
Member Author

@nielsdos it happens in shutdown_memory_manager(full_shutdown: false), which is called before every request

Copy link
Member

nielsdos commented Aug 9, 2025
edited
Loading

I see. I'm neutral to it, I like the protection but it is also limited in scope and doubles the minimum (削除) chunk size (削除ここまで) allocated memory (although not by a lot and it could be overcommitted).

Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_zend_ptr() looks like it will also need adjustments to loop through all zones.

The concept makes sense to me. Though I'd expect quite a lot of string operations can move user-controlled inputs to the default zone.

Isolate request environment / input in separate chunks to makes it more
difficult to remotely control the layout of the heap.
Copy link

AWS x86_64 (c7i.24xl)

Attribute Value
Environment aws
Runner host
Instance type c7i.metal-24xl (dedicated)
Architecture x86_64
CPU 48 cores
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 188 GB
Kernel 6.1.147-172.266.amzn2023.x86_64
OS Amazon Linux 2023年8月20日250818
GCC 11.5.0
Time 2025年08月27日 09:53:07 UTC

Laravel 12.2.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@0ce1 0.46244 0.47158 0.00118 0.25% 0.46914 0.00% 0.46902 0.00% -3.185 0.999 177372416 43.28 MB
PHP - mm-zones 0.47070 0.47510 0.00060 0.13% 0.47374 0.98% 0.47371 1.00% -1.074 0.000 177817819 43.57 MB

Symfony 2.7.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@0ce1 0.74021 0.75287 0.00184 0.25% 0.74235 0.00% 0.74199 0.00% 3.685 0.999 291652548 39.84 MB
PHP - mm-zones 0.74407 0.75144 0.00154 0.21% 0.74643 0.55% 0.74622 0.57% 1.784 0.000 292300931 39.84 MB

Wordpress 6.2 main page - 100 consecutive runs, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@0ce1 0.57916 0.58261 0.00057 0.10% 0.58025 0.00% 0.58020 0.00% 1.061 0.999 1129906527 43.43 MB
PHP - mm-zones 0.58203 0.58488 0.00063 0.11% 0.58329 0.52% 0.58316 0.51% 0.431 0.000 1133164084 43.40 MB

bench.php - 100 consecutive runs, 10 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@0ce1 0.43064 0.44237 0.00182 0.42% 0.43344 0.00% 0.43324 0.00% 2.640 0.999 2031002294 26.50 MB
PHP - mm-zones 0.43231 0.44449 0.00154 0.35% 0.43472 0.30% 0.43456 0.30% 2.735 0.000 2031419569 27.16 MB
arnaud-lb and iluuu1994 reacted with confused emoji

Copy link

AWS x86_64 (c7i.24xl)

Attribute Value
Environment aws
Runner host
Instance type c7i.metal-24xl (dedicated)
Architecture x86_64
CPU 48 cores
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 188 GB
Kernel 6.1.147-172.266.amzn2023.x86_64
OS Amazon Linux 2023年8月20日250818
GCC 11.5.0
Time 2025年08月27日 14:35:32 UTC

Laravel 12.2.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@e844 0.46793 0.47043 0.00054 0.11% 0.46895 0.00% 0.46894 0.00% 0.619 0.999 176985119 43.79 MB
PHP - mm-zones 0.46867 0.47345 0.00066 0.14% 0.47000 0.22% 0.46994 0.21% 1.573 0.000 177472648 43.94 MB

Symfony 2.7.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@e844 0.73395 0.74628 0.00135 0.18% 0.73755 0.00% 0.73722 0.00% 2.896 0.999 288205458 40.14 MB
PHP - mm-zones 0.73528 0.74088 0.00108 0.15% 0.73744 -0.02% 0.73736 0.02% 0.724 0.710 288853489 40.14 MB

Wordpress 6.2 main page - 100 consecutive runs, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@e844 0.57892 0.58211 0.00065 0.11% 0.58046 0.00% 0.58037 0.00% 0.316 0.999 1129136611 43.69 MB
PHP - mm-zones 0.58230 0.58563 0.00070 0.12% 0.58394 0.60% 0.58394 0.62% 0.024 0.000 1132667847 43.78 MB

bench.php - 100 consecutive runs, 10 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Instr count Memory
PHP - baseline@e844 0.43281 0.55522 0.02153 4.86% 0.44292 0.00% 0.43857 0.00% 4.723 0.999 2031379026 26.70 MB
PHP - mm-zones 0.43463 0.59267 0.02584 5.82% 0.44399 0.24% 0.43885 0.06% 5.021 0.269 2031828994 27.46 MB

Copy link

AWS x86_64 (c7i.24xl)

Attribute Value
Environment aws
Runner host
Instance type c7i.metal-24xl (dedicated)
Architecture x86_64
CPU 48 cores
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 188 GB
Kernel 6.1.147-172.266.amzn2023.x86_64
OS Amazon Linux 2023年8月20日250818
GCC 14.2.1
Time 2025年09月10日 11:20:33 UTC

Laravel 12.2.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@e844 0.46557 0.47251 0.00072 0.15% 0.46688 0.00% 0.46683 0.00% 4.886 0.999 44.21 MB
PHP - mm-zones 0.46476 0.47576 0.00093 0.20% 0.47126 0.94% 0.47120 0.94% -2.278 0.000 44.35 MB

Symfony 2.7.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@e844 0.72742 0.74320 0.00181 0.25% 0.73667 0.00% 0.73636 0.00% -0.075 0.999 40.59 MB
PHP - mm-zones 0.73853 0.74444 0.00116 0.16% 0.74056 0.53% 0.74037 0.54% 0.952 0.000 40.60 MB

Wordpress 6.2 main page - 100 consecutive runs, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@e844 0.57724 0.58246 0.00087 0.15% 0.57948 0.00% 0.57953 0.00% 0.326 0.999 44.07 MB
PHP - mm-zones 0.58034 0.58520 0.00098 0.17% 0.58214 0.46% 0.58196 0.42% 0.581 0.000 44.06 MB

bench.php - 100 consecutive runs, 10 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@e844 0.43050 0.56057 0.02053 4.70% 0.43647 0.00% 0.43293 0.00% 5.580 0.999 26.96 MB
PHP - mm-zones 0.42920 0.55515 0.01992 4.56% 0.43706 0.14% 0.43348 0.13% 5.559 0.004 27.74 MB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Reviewers

@iluuu1994 iluuu1994 iluuu1994 left review comments

@dstogov dstogov dstogov left review comments

@nielsdos nielsdos nielsdos left review comments

@Girgias Girgias Girgias left review comments

@bukka bukka Awaiting requested review from bukka bukka is a code owner

@devnexen devnexen Awaiting requested review from devnexen devnexen is a code owner

+1 more reviewer

@jvoisin jvoisin jvoisin left review comments

Reviewers whose approvals may not affect merge requirements
Assignees
No one assigned
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /