fix: 扫描结尾不再丢最后一个 MCU + 畸形 SOF 尺寸硬闸(M23 r2 P1-A/P1-B)

P1-A(像素错误):mj_boundary() 在「over 且 marker 非 RSTn」时 return 0 提前退出
扫描。熵码位缓冲最多 9 位前瞻,会在最后一个 MCU/块**之前**把扫描结束后的 marker
(DHT/EOI)吃掉;此时最后 1~2 个 MCU 的有效比特还在 bitbuf 里,return 0 把它们丢掉,
分量平面 memset(128) 的灰底直接露出来(低熵/纯白渐进式右下角 #808080)。
修:扫描循环本来就知道确定的 MCU/块数 —— 一律继续解完(位耗尽后 mj_eb 返回 0 做
零填充),marker 交给外层主循环。实测(纯白 2000x3000 progressive 无 restart):
修复前全图最小通道 128 / 与 stb 最大差 127 / >40 差值的通道 9 个;修复后
最小通道 253~255、最大差 ≤2、>40 通道 0。

测试加固(评审指出旧断言是抽样蒙混):
- S12「与无 restart 逐字节一致」改为**全图 memcmp**(去掉 64×64 网格 MAD<0.1 的兜底);
  修复后 9 档 restart 全部逐字节一致。
- 新增 S15:纯白 2000x3000 progressive(q50/60/70/80,无 restart)+ mostly-white q95,
  断言全图最小通道/右下角 24×24 最小通道 ≥250(丢 MCU 必露 128)、与 stb 全图逐像素比较。
  用 HEAD(11a4fcd) 的 meow_jpeg.c 重编同一测试:4 档纯白全部 FAIL(min=128,
  >40 通道 9),修复后全 PASS。

P1-B(内存放大/安全):mj_sof() 按 SOF 声明尺寸分配平面,降档循环在 base_n 已为 1
时不再生效,17~23 字节的 `SOI+SOF0(65535×65535)+EOI` 会让公开入口 _dim(720)
分配并 memset 64MiB(1 分量)/96MiB(3 分量)后才返回 NULL。
修:降档循环之后补硬闸 `if (plane_bytes + target_bytes > MJ_PEAK_CAP) return 0;`
→ 回退 stb(同文件 stb 是 0.02MiB 安全失败)。
回归 S16:构造 SOF 65535²(1/3 分量)→ 后端返回 0、公开入口 NULL、峰值 0.02MiB
(修复前 64.00 / 96.00MiB)。

meow_jpeg.h 注明「峰值只与输出尺寸相关」的适用范围:畸形/超大声明尺寸直接拒绝(安全失败)。

验证:test_meow_img_stream ALL PASS(含 ASan 整轮 rc=0 无报告);test_meow_img_budget /
test_meow_img_color ALL PASS;run_tests.sh rc=0(zip 106 / cbz 296 / membudget PASS);
run_webtoon_tests.sh rc=0;build_sdk.sh 零警告,MIPS32 rel2;体积 meow_jpeg.o 17,212B
+ meow_png_stream.o 8,456B。峰值不变(修复不增加分配)。
This commit is contained in:
AskaEth committed 2026-11-06 11:21:03 +08:00
1 parent 11a4fcd730
commit 6c8a1d3925
3 files changed
+192 -19

No files matched your search

+12 -11
View File
@@ -482,6 +482,10 @@ static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int
for (i = 0; i < n; i++) if (img->c[i].n > base_n) img->c[i].n = base_n;
plane_bytes = mj_layout(img);
}
/* 硬闸:降到 N=1 仍超上限(畸形/超大声明尺寸,如 23 字节的 SOF 65535² → 亮度面
* 8192² = 64MiB)直接拒绝 → 返回 0 → 回退 stb(stb 对这类文件是 KB 级安全失败)。
* 没有这道闸,公开入口 meow_img_load_file_dim(720) 会被一个头文件打爆设备内存。 */
if (plane_bytes + target_bytes > MJ_PEAK_CAP) return 0;
for (i = 0; i < n; i++) {
mj_comp *c = &img->c[i];
@@ -596,28 +600,25 @@ rst_done:
;
}
/* 区间边界处理(三处扫描循环共用)。返回:0 = 熵段结束(调用方退出扫描);
* 1 = 继续;2 = 刚做过区间重同步(AC 扫描需清 EOBRUN)。
/* 区间边界处理(三处扫描循环共用)。返回:1 = 继续;2 = 刚做过区间重同步(AC 需清 EOBRUN)。
*
* 只在 total == rst(编码器的区间长度)时重同步。**不能**一看到 over&&RSTn 就重同步:
* 熵码位缓冲有最多 9 位前瞻,`over` 会在区间真正结束前 1~2 个 MCU 就被置位,
* 此时 bitbuf 里还压着本区间剩余块的有效比特——提前重同步会把它们丢掉并失步
* 熵码位缓冲有最多 9 位前瞻,`over` 会在区间/扫描真正结束前 1~2 个 MCU 就被置位,
* 此时 bitbuf 里还压着剩余块的有效比特——提前重同步会把它们丢掉并失步
* (渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过整个 MCU;这是 rst=2..200 花屏的根因)。
* 到达 total==rst 时,RSTn 要么已被前瞻吃掉(over/marker=RSTn,清掉即可),
* 要么还在流里(裸扫对齐)——mj_restart 两种都处理。 */
* 同理,over 且 marker 非 RSTn(扫描结束后的 DHT/EOI 已被前瞻吃掉)时**也不能退出**:
* 扫描循环本来就知道确定的 MCU/块数,把剩余的解完即可(位耗尽后 mj_eb 返回 0 做零填充),
* marker 交给外层主循环处理。曾在此 `return 0` 提前退出,丢掉最后一个 MCU,
* 平面 memset(128) 的灰底直接露出来(低熵/纯白渐进式右下角 3 像素 #808080)。 */
static int mj_boundary(mj_img *img, int *total)
{
mj_src *s = &img->src;
(void)s;
if (img->rst && *total == img->rst) {
mj_restart(img);
*total = 0;
return 2;
}
if (s->over) {
/* 前瞻已吃到 RSTn:本区间剩余块的有效比特还在 bitbuf 里,继续解。
* 只有「非 RST 的 marker」才是熵段真的结束(EOI/下一段的 DHT…)。 */
return (s->marker >= 0xD0 && s->marker <= 0xD7) ? 1 : 0;
}
return 1;
}