php / php/php-src

SHM_PROTECT/SHM_UNPROTECT race in opcache under ZTS with multiple threads and protect_memory=1

Đang mở
#21,772 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
C
Star
40.4k
Fork
8.2k
Merge trung bình
2 ngày 15 giờ
Pull request đã merge (30 ngày)
103

Mô tả

Description

When multiple PHP threads run concurrently in the same process (ZTS build), opcache's SHM_PROTECT()/SHM_UNPROTECT() calls race with each other because mprotect() is process-global, but there is no coordination (refcounting) between threads.

I'm not 100% sure this is a bug vs. a known limitation. But the behavior is surprising and I wanted to report it for discussion.

What happens

SHM_PROTECT() calls mprotect(shm, PROT_READ) and SHM_UNPROTECT() calls mprotect(shm, PROT_READ|PROT_WRITE). These are process-wide — they affect all threads.

In ZEND_RINIT_FUNCTION(zend_accelerator) (ZendAccelerator.c, lines 2714–2778), every php_request_startup() executes an SHM_UNPROTECT() / SHM_PROTECT() pair. In a multi-threaded ZTS process, each thread calls php_request_startup() independently.

When tracing JIT is enabled and compiling a hot trace, it also holds SHM unprotected (zend_jit_trace.c, line 7512). If a second thread's RINIT calls SHM_PROTECT() while the first thread is still writing to SHM inside JIT compilation, the first thread gets SIGSEGV (write to read-only page).

Timeline:

Main thread                              Worker thread
───────────                              ─────────────
zend_jit_compile_root_trace:
  zend_shared_alloc_lock()
  SHM_UNPROTECT()  ← PROT_READ|WRITE
  writing to zend_jit_traces[N]...       php_request_startup():
  ...                                      RINIT(accel):
  ...                                        SHM_UNPROTECT() ← no-op
  ...                                        ...checks restart_pending...
  ...                                        SHM_PROTECT() ← mprotect(PROT_READ)
  ...                                        done, returns
  t->code_start = start  → SIGSEGV
  (page is now read-only)

The zend_shared_alloc_lock() serializes JIT compilation between threads, but it does NOT prevent RINIT's SHM_PROTECT() from running concurrently — RINIT doesn't acquire that lock for its SHM_UNPROTECT/PROTECT pair.

How to reproduce

Any ZTS setup with multiple threads running PHP code concurrently + opcache.protect_memory=1 + opcache.jit=tracing.

Minimal reproduction with TrueAsync threads (but should be reproducible with any ZTS threading extension — parallel, pmmpthread, etc.):

<?php
use Async\ThreadPool;
use function Async\spawn;
use function Async\await;

spawn(function() {
    $pool = new ThreadPool(2);
    $future = $pool->submit(fn() => 42);
    echo await($future) . "\n";
    $pool->close();
    echo "Done\n";
});

Run with:

php -d opcache.enable_cli=1 \
    -d opcache.jit=tracing \
    -d opcache.jit_buffer_size=64M \
    -d opcache.protect_memory=1 \
    -d opcache.jit_hot_loop=1 \
    -d opcache.jit_hot_func=1 \
    test.php

Result: correct output 42\nDone followed by SIGSEGV in zend_jit_trace_add_code (zend_jit_trace.c:227).

The crash does NOT happen with:

  • opcache.protect_memory=0 (default) — mprotect is never called, no race
  • opcache.jit=function — functions are compiled before threads start, no runtime compilation during concurrent execution
Notes
  • The krakjoe/parallel extension has the same architecture (calls php_request_startup() per thread) and avoids this by only testing with opcache.jit=function or opcache.jit=disable in CI. Their ASAN+JIT job explicitly uses -d opcache.jit=function.
  • run-tests.php hardcodes opcache.protect_memory=1 (line 300), so any multi-threaded test suite using run-tests.php with tracing JIT will hit this.
  • The underlying issue is that mprotect() is process-global but SHM_UNPROTECT/PROTECT pairs are not coordinated across threads — there is no refcount to prevent one thread's PROTECT from overriding another thread's active UNPROTECT window.
PHP Version
PHP 8.6.0-dev (cli) (ZTS DEBUG)
Zend Engine v4.6.0-dev
    with Zend OPcache v8.6.0-dev
Operating System

Ubuntu 24.04 (Linux 6.17)

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu với cặp SHM_PROTECT/SHM_UNPROTECT trong ZendAccelerator.c ở các dòng 2714–2778, sau đó kiểm tra cửa sổ JIT trong zend_jit_trace.c quanh dòng 7512 và vị trí crash ở dòng 227. Tái hiện bằng một bản build ZTS sử dụng script TrueAsync được cung cấp cùng các tùy chọn tracing-JIT. Được coi là hoàn tất khi bài kiểm thử đồng thời không còn chạm tới SIGSEGV và đã xác định được phạm vi bao phủ hồi quy phù hợp.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
c, php
Lĩnh vực
backend, performance
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
38/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.