Bucket brigade UAF
- Dominant language
- C
- Stars
- 40.4k
- Forks
- 8.1k
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 96
Description
Description
php_user_filter's filter($in, $out, ...) receives $in and $out as PHP resources created by zend_register_resource() in userfilter_filter() but those php_stream_bucket_brigade structures are allocated on the C stack of the calling function (brig_a/brig_b in _php_stream_filter_flush, filter.c:452; brig_in/brig_out in _php_stream_filter_append filter.c:366). The le_bucket_brigade resource type has no destructor (user_filters.c:91), and after the callback returns the code only runs zval_ptr_dtor() on its local references (user_filters.c:249-250) without ever calling zend_list_close() so a resource copy stashed by userland (e.g. $GLOBALS['x'] = $in;) survives with res->ptr still pointing at the now-freed stack frame. When that stale resource is later passed to stream_bucket_make_writeable() or stream_bucket_append(), zend_fetch_resource() succeeds (it only checks the resource type, not liveness) and returns the dangling brigade pointer, which is then dereferenced - a use-after-free read and via brigade->tail->next = bucket a use-after-free write into freed stack memory. The root cause is that a request-lived PHP resource is allowed to outlive its stack-allocated backing store without being invalidated. The fix is to call zend_list_close(Z_RES(args[0])) / zend_list_close(Z_RES(args[1])) immediately after the filter() call (before the zval_ptr_dtors) so any stored copies become non-fetchable, or to stop backing these resources with stack memory.
PHP Version
8.6.0-dev
Operating System
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.