根本原因是PCRE底层递归匹配引擎在协程固定大小C栈(默认2MB)中发生深度回溯,超出stack_size导致栈溢出;协程栈独立于PHP用户态栈,且PCRE直接压C层调用栈,高危正则如(a+)+遇恶意输入会迅速耗尽栈空间。

Hyperf 中 PCRE 正则匹配触发协程栈溢出,根本原因不是正则本身写错了,而是默认 coroutine.stack_size(2MB)被 PCRE 递归回溯吃光,导致 mmap 频繁扩容、内存碎片、最终崩溃或 CPU 飙升。
为什么 PCRE 在协程里特别容易栈溢出
PHP 的 PCRE(preg_match、preg_replace 等)底层使用递归匹配引擎。当正则存在大量嵌套括号、贪婪量词 + 回溯(如 .* 配合复杂边界)时,C 层调用栈深度可能轻松突破 1000 层。而 Swoole 协程栈是固定大小的连续内存块,默认 coroutine.stack_size = 2097152(2MB),一旦 PCRE 递归压栈超过该阈值,就会触发栈保护机制——抛出 Fatal error: Stack overflow 或静默 segfault(尤其在高并发下表现为 worker 进程异常退出)。
- 协程栈 ≠ PHP 用户态栈:PCRE 的 C 层递归直接压的是协程的 C stack,不是 PHP 的
zend_stack - 问题在 Swoole 层:Hyperf 本身不控制栈大小,它依赖 Swoole 的
coroutine.stack_size配置 - 不是所有正则都爆:只有「回溯爆炸」类正则(如
^(a+)+$匹配aaaaaaaaX)才高危
怎么安全调小 coroutine.stack_size
盲目调大栈(比如设成 4MB)只会掩盖问题,且加剧内存占用(每个协程独占该空间)。正确做法是:**显式调小 + 配合系统限制校准 + 避免高危正则**。
- 在
config/autoload/swoole.php中明确设置:'coroutine' => ['stack_size' => 524288](512KB,比默认低 75%) - 必须同步检查系统线程栈限制:
ulimit -s,若返回8192(8MB)或更高,说明没问题;若低于 1024,需先执行ulimit -s 8192(临时)或写入/etc/security/limits.conf(永久) - 512KB 是平衡点:足够支撑绝大多数业务正则(包括含 10 层嵌套的 JSON Path 类匹配),又避免单协程吃掉过多内存(worker_num=32 时,总栈内存从 64MB 降至 16MB)
- 切勿设低于 262144(256KB):Swoole 自身调度、Hyperf Context 初始化等基础开销已接近此值
如何识别和规避高危 PCRE 正则
栈溢出往往发生在特定输入上,测试时不易暴露。应主动扫描代码中潜在风险点:
- 禁用无边界贪婪匹配:
.*、.+必须配合原子组或占有量词,例如改用(?>.*)或.*+ - 避免嵌套量词:
(a+)+、(\d{1,3}\.){3}\d{1,3}(IP 正则)应拆解或用filter_var($ip, FILTER_VALIDATE_IP)替代 - 对用户可控输入的正则做白名单限制:比如只允许
^[a-zA-Z0-9_-]{3,20}$这类简单模式 - 用
set_error_handler捕获E_ERROR并记录触发正则内容,便于事后分析
协程栈溢出的真实错误现象
它不会总是报 “Stack overflow”。常见表现更隐蔽:
- Worker 进程无声退出,
ps aux | grep hyperf发现进程数波动 - 日志中出现
Segmentation fault (core dumped),但无 PHP 堆栈 -
strace -p {worker_pid}显示大量mmap调用失败或brk异常 - 同一段正则在 CLI 模式下正常,在 Hyperf 协程中必崩(CLI 是 OS 线程栈,通常 8MB;协程是独立小栈)
真正关键的不是“调多大”,而是理解 PCRE 递归与协程栈的耦合关系——哪怕设成 1MB,遇到 ^(x+x+)+y$ 这种经典 ReDoS 模式,照样秒崩。所以栈配置只是兜底,正则治理才是根因。


















