3.2.12 紧急修复:点「测试邮件」引发无限递归、内存耗尽
3.2.11 引入的事故。线上表现:
Allowed memory size of 268435456 bytes exhausted
in .../wp-includes/class-wp-block-parser.php on line 91
前台首页、后台登录页都正常,只有点那个按钮才炸。
根因不在区块解析器 —— 那只是递归链最后执行到的一行,看它会完全跑偏。
validate_options() 是 register_setting() 注册的 sanitize_callback,
WP 把它挂在 `sanitize_option_kratos` 过滤器上(wp-includes/option.php:3073)。
3.2.11 为了让"刚改的 SMTP 配置立刻生效",在这个回调里调了
update_option('kratos', $clean)。而 update_option() 内部**会再跑一遍**
同名过滤器 ⇒ 又调回 validate_options ⇒ 再 update_option ⇒ 无限递归。
每层都在复制 $clean 派生的大数组,直到吃满 256M。
本地实测确认递归存在:回调被调用 3 次、嵌套深度 2。
改法:不写库,改用一次性覆盖层。
- 分支里把 $clean 放进 $GLOBALS['__yang_options_override']
- kratos_option() 优先读它,取不到再读库
- 效果等价(点测试用的就是刚填的值),但不触发任何过滤器,不可能递归
- 覆盖层是本请求内的全局,不落库、不跨请求残留
assertions: run_tests 104 → 110
- 分支里**没有** update_option(这条是红线,直接对应本次事故)
- 分支里有覆盖层
- 覆盖层真生效:kratos_option 优先返回新值
- 覆盖层未命中的键仍读库(不能把别的配置一起抹掉)
- 撤掉覆盖层回到库值(不跨请求残留)
- 桩与产品代码一致:直接读 autoload.php 确认它也认覆盖层
(桩比产品温和是这次漏检的根源之一 —— 桩里 update_option 不挂
sanitize 过滤器,所以注入递归 bug 后 harness 根本不崩)
顺带修了两个测试基础设施问题:
1. wp_stub.php 的 kratos_option 与真实实现语义不一致。已同步,
并加断言防止两边再次漂移(否则"测的是桩、不是产品代码")。
2. falsify 的 rebuild() 没把被 file_get_contents 读的 autoload.php
复制进副本 ⇒ 新断言读不到文件 ⇒ 基线假红。
falsify 24 → 25:注入「分支里调 update_option」能抓住(靠静态红线,
不靠跑崩)、注入「去掉覆盖层」能抓住、注入「退回裸 wp_mail」能抓住。
全量 14 组全绿;run_tests 110 / run_tests2 154 / falsify 25。
This commit is contained in:
@@ -71,6 +71,26 @@ if (!function_exists('kratos_option')):
|
|||||||
function kratos_option($name, $default = false)
|
function kratos_option($name, $default = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/* 一次性覆盖层:只影响当前这次请求。
|
||||||
|
*
|
||||||
|
* 用在「后台点测试邮件」这一种场景:那时用户刚在表单里改了
|
||||||
|
* SMTP 主机/账号/密码,但新值还没写进库(register_setting 的
|
||||||
|
* sanitize 回调返回值之后才落库)。直接读库会拿到上一次的配置,
|
||||||
|
* 于是"明明填对了却发不出去"。
|
||||||
|
*
|
||||||
|
* 为什么不用 update_option() 把新值先写进去:那个函数会再触发
|
||||||
|
* 一次 sanitize_option_kratos 过滤器,而写入逻辑本身就在那个
|
||||||
|
* 回调里 ⇒ 无限递归吃爆内存(3.2.11 的事故)。
|
||||||
|
*
|
||||||
|
* 覆盖层只在本请求内有效、不落库、不触发任何过滤器,
|
||||||
|
* 所以安全。其它场景这个全局变量根本不存在,行为与原来一致。
|
||||||
|
*/
|
||||||
|
if (isset($GLOBALS['__yang_options_override'])
|
||||||
|
&& is_array($GLOBALS['__yang_options_override'])
|
||||||
|
&& array_key_exists($name, $GLOBALS['__yang_options_override'])) {
|
||||||
|
return $GLOBALS['__yang_options_override'][$name];
|
||||||
|
}
|
||||||
|
|
||||||
$option_name = 'kratos';
|
$option_name = 'kratos';
|
||||||
|
|
||||||
// Get option settings from database
|
// Get option settings from database
|
||||||
|
|||||||
@@ -255,15 +255,27 @@ class Options_Framework_Admin {
|
|||||||
|
|
||||||
if ( isset( $_POST['sendmail'] ) ) {
|
if ( isset( $_POST['sendmail'] ) ) {
|
||||||
/*
|
/*
|
||||||
* 测试发信必须先落库再发。
|
* 测试发信要让「这一批新配置」立刻生效,但**绝对不能**
|
||||||
|
* 在这里调 update_option()。
|
||||||
*
|
*
|
||||||
* 这个函数是 register_setting 的回调,返回值**之后**才会被
|
* ── 为什么不调(3.2.11 的血案)──────────────────────────
|
||||||
* update_option() 写进数据库。如果在这里直接 wp_mail(),
|
* 本函数是 register_setting() 注册的 sanitize_callback,
|
||||||
* 主题配置 PHPMailer 时读 kratos_option() 拿到的还是**上一次**保存的
|
* WP 把挂在 `sanitize_option_kratos` 这个过滤器上
|
||||||
* 主机/账号/密码 —— 刚改完配置点测试,测的其实是旧配置,
|
* (wp-includes/option.php:3073)。而 update_option() 内部
|
||||||
* 用户会看到"明明填对了却发不出去"或"改了密码还能发"这种怪现象。
|
* **会再跑一遍**同名过滤器 ⇒ 又调回本函数 ⇒ 本函数再
|
||||||
|
* update_option() ⇒ 无限递归。
|
||||||
|
*
|
||||||
|
* 递归每层都在复制 $clean 和 $_POST 派生的大数组,最终把
|
||||||
|
* 内存吃到上限,报出来的是
|
||||||
|
* 「Allowed memory size of 268435456 bytes exhausted
|
||||||
|
* in class-wp-block-parser.php」。报错落在区块解析器上纯属
|
||||||
|
* 巧合 —— 那只是递归链最后执行到的一行,看它会完全找错方向。
|
||||||
|
*
|
||||||
|
* 正确做法:不写库、不递归,只把新值放进一个一次性覆盖层,
|
||||||
|
* 让 kratos_option() 在本次请求里优先读它。写完库由 WP 自己
|
||||||
|
* 在 validate_options 返回后完成。
|
||||||
*/
|
*/
|
||||||
update_option( 'kratos', $clean );
|
$GLOBALS['__yang_options_override'] = is_array( $clean ) ? $clean : array();
|
||||||
|
|
||||||
$this->yang_send_test_mail( $clean );
|
$this->yang_send_test_mail( $clean );
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Theme Name: Kratos_Yang
|
Theme Name: Kratos_Yang
|
||||||
Text Domain: kratos_Yang
|
Text Domain: kratos_Yang
|
||||||
Version: 3.2.11
|
Version: 3.2.12
|
||||||
Requires PHP: 7.0
|
Requires PHP: 7.0
|
||||||
Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。
|
Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。
|
||||||
Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好
|
Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好
|
||||||
|
|||||||
Reference in New Issue
Block a user