sdk: 修 ILP32 尺寸算术溢出 — 畸形 BMP 经公开入口 SIGSEGV(与主仓同源补丁)

meow_png_stream.c 的 BMP 路径用 32 位 long 算 rowsize=(w*bpp+31)/32*4,
w≥2^27 且 bpp=32 时 w*bpp 回绕 → rowsize=0 → malloc(0),随后采样下标
sx=(int)((long)tx*w/tw) 同样溢出成负数,row+sx*4 越界读。

实测(zig cc --target=arm-linux-musleabi,ILP32,与 mipsel 同数据模型):
  修复前 w=0x07FFFFFF → r=0(安全)
          w=0x08000000 → SIGSEGV(139)   只需 54 字节 BMP 头
          w=0x10000000/0x40000000 → SIGSEGV
  同一文件在 LP64 宿主 → NULL,宿主测试天然看不到这一类
  修复前走 stb 路径 → NULL,即合并图像栈后属行为退化

改法(只加闸,合法尺寸的数值路径不变):
- MZP_MAX_DIM (1<<20) 守卫 + mzp_bytes_ok() 64 位校验
- 声明尺寸先取 int 再升 long long;h=INT_MIN 用 -(long long)hi 取绝对值
- rowsize / 行偏移 off / 采样下标 sx 全部升 64 位,sx 夹在 [0,w-1]
- fseek 前 off > LONG_MAX 拒绝;目标 RGBA 字节数 64 位验后再 malloc
- meow_jpeg.c 同类审计:mj_layout() 返回 unsigned long long + MJ_LAYOUT_BAD、
  mj_sof() 的 target_bytes/plane、mj_output() 的 y*vh/x*vw、
  meow_img_target_dims() 与 mj_pick_n() 的 h*maxdim 全部升 64 位

新增 S17 回归(10 组畸形 BMP + 2 个合法正例 + 1 个 PNG 回绕点)。
可证伪性:旧代码 + 新测试在 ARM32 上 exit 139,新代码 ALL PASS。
S17 的 32 位覆盖需 ARM32 交叉编译(宿主 LP64 只能固定 NULL 契约),
复现命令写在 test_meow_img_stream.c 文件头的「ILP32 回归」小节。

验证:本仓 test_meow_img_stream ALL PASS;sdk/build_sdk.sh 34 成员;
tuku 四套脚本全绿(run_tests 644 PASS / zip 106 / cbz 296 / webtoon PASS)。
This commit is contained in:
AskaEth committed 2026-11-06 21:38:47 +08:00
1 parent 27a3750e36
commit 342c612358
3 files changed
+300 -47

No files matched your search

+157
View File
@@ -12,6 +12,9 @@
* S8 内存入口 == 文件入口(逐字节),峰值同级
* S9 回退:CMYK JPEG(4 分量)、1bpp BMP、Adam7 交错 PNG → 后端拒绝,stb 仍成功;
* 截断/垃圾 → NULL(不崩)
* S10-S16 缩解阶/restart/DHT/tRNS/step/低熵/progressive-restart 等专项
* S17 32 位尺寸算术:畸形 BMP 声明尺寸(w=2^27…、24bpp 回绕点、h=INT_MIN)与 PNG
* tw*th*4 回绕点 → 一律 NULL 不崩(ILP32 崩溃回归,M26 r1 P1)
*
* 夹具:本文件内置生成脚本(Pillow),首次运行时写到夹具目录并执行。
* 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点):
@@ -20,6 +23,17 @@
* -lm -lz \
* -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
* 运行:./test_meow_img_stream "$PREFIX/tmp/meow_img_stream" (exit 0=PASS / 1=FAIL)
*
* ILP32 回归(S17 的真覆盖;宿主 LP64 的 long/size_t 是 64 位,看不到 32 位回绕崩溃):
* 用 zig 的 arm-linux-musleabi 目标(与 mipsel 同数据模型:long/size_t 均 4 字节),
* 需要一份 32 位 libz.a(设备上的 libz.a 就是 mipsel 32 位;本机可从 zlib 源码
* 用同一 zig cc --target=arm-linux-musleabi -c 各 .c 后 ar 打包)。命令:
* zig cc --target=arm-linux-musleabi -static -O2 -I. -Iinclude -DMEOW_UI_HOST_TEST \
* -o test_meow_img_stream_arm test_meow_img_stream.c meow_ui_img.c meow_jpeg.c \
* meow_png_stream.c stb_image.c -lm <libz_arm.a> \
* -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
* ./test_meow_img_stream_arm "$PREFIX/tmp/meow_img_stream"
* (aarch64 Android 设备可直跑 32 位 ARM 静态二进制;2026-11-06 实测 S1-S17 全绿。)
*/
#define _GNU_SOURCE
#include "meow_ui.h"
@@ -566,6 +580,60 @@ static size_t build_sof_only(unsigned char *buf, int w, int h, int ncomp)
return p;
}
/* ---- S17 用的畸形头构造器(54 字节 BMP / 仅 IHDR 的 PNG) ---- */
static size_t bmp_build(unsigned char *b, unsigned w, unsigned hbits, unsigned bpp)
{
size_t off = 54;
memset(b, 0, off);
b[0] = 'B'; b[1] = 'M';
b[2] = (unsigned char)(off & 0xFF); b[3] = (unsigned char)((off >> 8) & 0xFF);
b[10] = (unsigned char)(off & 0xFF); b[11] = (unsigned char)((off >> 8) & 0xFF);
b[14] = 40; /* BITMAPINFOHEADER */
b[18] = (unsigned char)(w & 0xFF); b[19] = (unsigned char)((w >> 8) & 0xFF);
b[20] = (unsigned char)((w >> 16) & 0xFF); b[21] = (unsigned char)((w >> 24) & 0xFF);
b[22] = (unsigned char)(hbits & 0xFF); b[23] = (unsigned char)((hbits >> 8) & 0xFF);
b[24] = (unsigned char)((hbits >> 16) & 0xFF); b[25] = (unsigned char)((hbits >> 24) & 0xFF);
b[26] = 1; /* planes */
b[28] = (unsigned char)(bpp & 0xFF);
return off;
}
/* 正例对照:合法的 64×64 32bpp BMP(topdown=1 时 h 写成负数) */
static size_t bmp_build_64(unsigned char *b, int topdown)
{
const unsigned w = 64, h = 64, bpp = 32;
size_t n = bmp_build(b, w, topdown ? (unsigned)(-64) : h, bpp);
unsigned x, y;
b[2] = (unsigned char)((n + w * h * 4) & 0xFF); /* 文件大小(解析器不校验) */
b[3] = (unsigned char)(((n + w * h * 4) >> 8) & 0xFF);
for (y = 0; y < h; y++)
for (x = 0; x < w; x++, n += 4) {
b[n + 0] = (unsigned char)(x * 4);
b[n + 1] = (unsigned char)(y * 4);
b[n + 2] = 0x40;
b[n + 3] = 0xFF;
}
return n;
}
static size_t png_ihdr_only(unsigned char *b, unsigned w, unsigned h)
{
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
size_t p = 0;
memcpy(b, sig, 8);
p = 8;
b[p++] = 0; b[p++] = 0; b[p++] = 0; b[p++] = 13; /* IHDR 长度 */
memcpy(b + p, "IHDR", 4); p += 4;
b[p++] = (unsigned char)(w >> 24); b[p++] = (unsigned char)((w >> 16) & 0xFF);
b[p++] = (unsigned char)((w >> 8) & 0xFF); b[p++] = (unsigned char)(w & 0xFF);
b[p++] = (unsigned char)(h >> 24); b[p++] = (unsigned char)((h >> 16) & 0xFF);
b[p++] = (unsigned char)((h >> 8) & 0xFF); b[p++] = (unsigned char)(h & 0xFF);
b[p++] = 8; /* bit depth */
b[p++] = 2; /* color type RGB */
b[p++] = 0; b[p++] = 0; b[p++] = 0;
return p; /* 故意不带后续块 */
}
/* ================================================================== */
int main(int argc, char **argv)
{
@@ -1117,6 +1185,95 @@ int main(int argc, char **argv)
}
}
/* ---- S17 32 位尺寸算术(ILP32 崩溃回归,M26 r1 P1) ----
* 54 字节 BMP 头声明 w=2^27、bpp=32 时,ILP32 上 w*bpp = 2^32 → rowsize 回绕成 0 →
* malloc(0) → 采样索引 row + sx*4 越界读 → SIGSEGV。LP64 宿主不会回绕,所以本节在
* 宿主的价值是「契约固定」:这些声明尺寸必须一律返回 NULL(且不崩);真正的 32 位
* 覆盖用法见文件头「ILP32 回归」那条交叉编译命令。 */
printf("[S17] 32 位尺寸算术:BMP 声明尺寸 / 采样偏移溢出(ILP32 崩溃回归)\n");
{
static const struct { const char *tag; unsigned w, hbits, bpp; int must_null; } sc[] = {
{ "w=0x07FFFFFF bpp=32(回绕点前一点)", 0x07FFFFFFu, 1u, 32u, 1 },
{ "w=0x08000000 bpp=32(2^27,旧代码崩点)", 0x08000000u, 1u, 32u, 1 },
{ "w=0x10000000 bpp=32", 0x10000000u, 1u, 32u, 1 },
{ "w=0x40000000 bpp=32", 0x40000000u, 1u, 32u, 1 },
{ "w=0x0AAAAAAA bpp=24(w*24 回绕点前)", 0x0AAAAAAAu, 1u, 24u, 1 },
{ "w=0x0AAAAAAB bpp=24(w*24 ≥ 2^32)", 0x0AAAAAABu, 1u, 24u, 1 },
/* 合法尺寸(≤1<<20)但无像素数据:后端必须拒绝;公开入口按设计回退 stb,
* stb 对截断 BMP 的行为不受本单约束,故只断言「不崩」,不要求 NULL。 */
{ "w=1000000 bpp=32(合法尺寸但无像素数据)", 1000000u, 1u, 32u, 0 },
{ "w=0x00100000 bpp=8(单边上限 1<<20)", 0x00100000u, 1u, 8u, 0 },
{ "w=0x00100001 bpp=8(超单边上限)", 0x00100001u, 1u, 8u, 1 },
{ "h=0x80000000(-INT_MIN 溢出点)", 0x01000000u, 0x80000000u, 32u, 1 },
};
for (size_t i = 0; i < sizeof sc / sizeof sc[0]; i++) {
unsigned char buf[54];
int tw = 0, th = 0;
unsigned char *px = NULL;
meow_img *im;
char name[200];
size_t base, pk;
char *fp = path_join("evil_dims.bmp");
FILE *f;
bmp_build(buf, sc[i].w, sc[i].hbits, sc[i].bpp);
snprintf(name, sizeof name, "%s → 后端拒绝", sc[i].tag);
base = peak_reset();
CHECK(meow_bmp_decode_stream(NULL, buf, sizeof buf, 720, &tw, &th, &px) == 0, name);
pk = peak_since(base);
CHECK(px == NULL, "拒绝时不返回像素缓冲");
if (sc[i].must_null) CHECK(pk < PEAK_2500KB, "越界声明在算术前就被拒(不分配)");
f = fopen(fp, "wb");
CHECK(f != NULL, "写畸形 BMP 到夹具目录");
if (f) { fwrite(buf, 1, sizeof buf, f); fclose(f); }
im = meow_img_load_file_dim(fp, 720);
snprintf(name, sizeof name, "%s → _dim 不崩%s", sc[i].tag,
sc[i].must_null ? "且返回 NULL" : "");
CHECK(im == NULL || !sc[i].must_null, name);
meow_img_free(im);
im = meow_img_load_mem_dim(buf, (int)sizeof buf, 720);
snprintf(name, sizeof name, "%s → _mem_dim 不崩%s", sc[i].tag,
sc[i].must_null ? "且返回 NULL" : "");
CHECK(im == NULL || !sc[i].must_null, name);
meow_img_free(im);
free(fp);
}
/* 正例对照:合法 64×64 32bpp 必须仍能解(含 h<0 的正上方行序) */
{
unsigned char *big = (unsigned char *)malloc(54 + 64 * 64 * 4);
int tw = 0, th = 0;
unsigned char *px = NULL;
size_t n;
CHECK(big != NULL, "分配正例 BMP 缓冲");
if (big) {
n = bmp_build_64(big, 0);
CHECK(meow_bmp_decode_stream(NULL, big, n, 32, &tw, &th, &px) == 1 &&
tw == 32 && th == 32, "正例:64x64 32bpp 正序 → 32x32");
free(px); px = NULL; tw = th = 0;
n = bmp_build_64(big, 1);
CHECK(meow_bmp_decode_stream(NULL, big, n, 32, &tw, &th, &px) == 1 &&
tw == 32 && th == 32, "正例:64x64 32bpp 倒序(h<0)→ 32x32");
free(px);
free(big);
}
}
/* PNG:tw*th*4 在 ILP32 上回绕成 0 的坐标点(65535² 头 + maxdim=32768) */
{
unsigned char pb[64];
int tw = 0, th = 0;
unsigned char *px = NULL;
size_t pn = png_ihdr_only(pb, 65535, 65535);
CHECK(meow_png_decode_stream(NULL, pb, pn, 32768, &tw, &th, &px) == 0,
"PNG 65535² maxdim=32768 → 拒绝(目标图字节数会回绕)");
CHECK(px == NULL, "PNG 拒绝时不返回像素缓冲");
}
}
/* ---- S8 内存入口 ---- */
printf("[S8] 内存入口 meow_img_load_mem_dim\n");
{