Files
ligent/tests/fuzz.c
T
2026-10-03 14:13:21 +08:00

346 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* fuzz.c - 对抗性模糊测试:JSON 解析器 + Markdown 渲染器
*
* 这两个模块是"解析不可信输入"的地方(服务端响应、模型输出),
* 也是最可能出崩溃/越界/泄漏的地方。所以用随机输入去砸它们。
*
* 检查的不变量:
* JSON
* 1. 任何输入都不能崩溃、不能泄漏(靠 ASan/UBSan/LeakSanitizer)
* 2. 解析成功 => dump 再 parse 再 dump,两次 dump 必须一致(幂等)
* 3. 随机生成的合法 JSON 必须解析成功并完整往返
* Markdown
* 4. 任何输入都不能崩溃、不能泄漏
* 5. 分片不变性:一次性喂 与 任意分片喂 的输出必须逐字节一致
* 6. 不含任何元字符的输入必须原样输出
*
* 用法: fuzz [迭代次数] [随机种子]
*/
#include "json.h"
#include "markdown.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
static uint64_t RNG;
static uint32_t rnd(void)
{
RNG ^= RNG << 13;
RNG ^= RNG >> 7;
RNG ^= RNG << 17;
return (uint32_t)(RNG >> 32);
}
static uint32_t rnd_below(uint32_t n) { return n ? rnd() % n : 0; }
/* ------------------------------------------------------------------ */
/* JSON */
/* ------------------------------------------------------------------ */
static const char *STR_POOL[] = {
"", "a", "hello", "中文", "😀", "line\nbreak", "tab\there", "quote\"x",
"back\\slash", "ctrl\x01\x1f", "surrogate\u00e9", "aaaaaaaaaaaaaaaaaaaa",
"/", "\x7f", "\xc3\xa9\xe4\xb8\xad",
};
static void gen_json_string(StrBuf *sb)
{
const char *s = STR_POOL[rnd_below(sizeof STR_POOL / sizeof STR_POOL[0])];
char *q = json_quote_string(s);
sb_puts(sb, q);
free(q);
}
/* 递归生成合法 JSON */
static void gen_json(StrBuf *sb, int depth)
{
uint32_t kind = rnd_below(depth > 3 ? 4 : 6);
switch (kind) {
case 0: sb_puts(sb, "null"); break;
case 1: sb_puts(sb, rnd() & 1 ? "true" : "false"); break;
case 2: {
char buf[64];
int mode = (int)rnd_below(3);
if (mode == 0) snprintf(buf, sizeof buf, "%d", (int)rnd() - 1000);
else if (mode == 1) snprintf(buf, sizeof buf, "%.6g", (double)rnd() / 7.0);
else snprintf(buf, sizeof buf, "%de%d", (int)rnd_below(1000), (int)rnd_below(20) - 10);
sb_puts(sb, buf);
break;
}
case 3: gen_json_string(sb); break;
case 4: {
sb_putc(sb, '[');
uint32_t n = rnd_below(5);
for (uint32_t i = 0; i < n; i++) {
if (i) sb_putc(sb, ',');
gen_json(sb, depth + 1);
}
sb_putc(sb, ']');
break;
}
default: {
sb_putc(sb, '{');
uint32_t n = rnd_below(5);
for (uint32_t i = 0; i < n; i++) {
if (i) sb_putc(sb, ',');
gen_json_string(sb);
sb_putc(sb, ':');
gen_json(sb, depth + 1);
}
sb_putc(sb, '}');
break;
}
}
}
/* 返回 1 表示这一轮失败 */
static int fuzz_json_once(char *err, size_t errlen)
{
StrBuf sb;
sb_init(&sb);
int mutate = (int)rnd_below(2);
if (mutate) {
/* 变异模式:先造合法 JSON,再随机破坏 */
gen_json(&sb, 0);
size_t n = sb.len;
if (n == 0) { sb_free(&sb); return 0; }
/* 每次突变都必须基于"当前"长度。早期版本用了突变前记下的旧长度,
* 截断之后再按下标写就会覆盖掉结尾的 '\0',后续 strlen 冲出分配区,
* 让 ASan 在 json_parse 里报出假阳性 —— 模糊测试器自己不可信比没有更糟。 */
uint32_t muts = 1 + rnd_below(4);
for (uint32_t i = 0; i < muts && sb.len > 0; i++) {
uint32_t op = rnd_below(3);
if (op == 0) { /* 翻一个字节(下标恒 < len) */
sb.data[rnd_below((uint32_t)sb.len)] = (char)(rnd() & 0xFF);
} else if (op == 1) { /* 截断 */
sb.len = rnd_below((uint32_t)sb.len);
} else { /* 插入一个字节 */
if (sb.len < 4096) {
size_t pos = rnd_below((uint32_t)sb.len + 1);
sb_reserve(&sb, 1);
memmove(sb.data + pos + 1, sb.data + pos, sb.len - pos + 1);
sb.data[pos] = (char)"{}[]\",:0123456789.eE+-truefalsn"[rnd_below(30)];
sb.len++;
}
}
}
/* 无论怎么突变,结尾符必须是好的 */
if (sb.data) sb.data[sb.len] = '\0';
n = sb.len;
} else {
/* 纯随机字节 */
uint32_t n = rnd_below(256);
for (uint32_t i = 0; i < n; i++) {
/* 偏向 JSON 元字符,提高触发率 */
static const char meta[] = "{}[]\",:0123456789.eE+-truefalsn \t\n\\/u";
char c = (rnd() & 3) ? meta[rnd_below(sizeof meta - 1)]
: (char)(rnd() & 0xFF);
sb_putc(&sb, c);
}
}
const char *input = sb.data ? sb.data : "";
size_t inlen = sb.len;
/* 输入里不能有 NUL,否则不是合法的 C 字符串测试对象 */
for (size_t i = 0; i < inlen; i++) {
if (input[i] == '\0') {
sb_free(&sb);
return 0;
}
}
/* 交给解析器的必须是真正 NUL 结尾的缓冲区 */
if (strlen(input) != inlen) {
snprintf(err, errlen, "测试器自身出错:缓冲区未正确结尾");
sb_free(&sb);
return 1;
}
JsonValue *v = json_parse(input, err, errlen);
if (v) {
char *d1 = json_dump(v);
char *d2 = NULL;
JsonValue *v2 = json_parse(d1, err, errlen);
if (!v2) {
snprintf(err, errlen, "解析成功的 JSON 却无法重新解析:%.120s", d1);
free(d1);
json_free(v);
sb_free(&sb);
return 1;
}
d2 = json_dump(v2);
if (strcmp(d1, d2) != 0) {
snprintf(err, errlen, "dump 不幂等:\n 输入 %.200s\n 一次 %.200s\n 二次 %.200s",
input, d1, d2);
free(d1); free(d2);
json_free(v); json_free(v2);
sb_free(&sb);
return 1;
}
free(d1); free(d2);
json_free(v2);
json_free(v);
}
sb_free(&sb);
return 0;
}
/* ------------------------------------------------------------------ */
/* Markdown */
/* ------------------------------------------------------------------ */
#define MD_RENDER_LIMIT (1u << 20)
/* 渲染到内存;chunk==0 表示一次性 */
static char *md_render(const char *src, size_t len, size_t chunk)
{
char *buf = NULL;
size_t size = 0;
FILE *f = open_memstream(&buf, &size);
if (!f) return NULL;
MdRenderer *r = md_new(1);
md_set_out(r, f);
if (chunk == 0) {
md_feed(r, src);
} else {
for (size_t i = 0; i < len; i += chunk) {
size_t n = len - i < chunk ? len - i : chunk;
char *tmp = xstrndup(src + i, n);
md_feed(r, tmp);
free(tmp);
}
}
md_finish(r);
md_free(r);
fclose(f);
return buf;
}
static int markdown_metachar(int c)
{
/* 这些字符会触发渲染器改写输出;其余字符必须原样通过 */
return c == '*' || c == '_' || c == '`' || c == '[' || c == ']' ||
c == '#' || c == '>' || c == '-' || c == '+' || c == '|' ||
c == '\n' || c == '\r';
}
static int fuzz_md_once(char *err, size_t errlen)
{
StrBuf sb;
sb_init(&sb);
static const char alpha[] =
"#*_-+>`[]|\\ \tabc123.:/~\n\r\u4e2d\U0001f600\"'()=;,\xc3\xa9";
size_t alen = sizeof alpha - 1;
uint32_t n = rnd_below(400);
int pure = (int)rnd_below(4) == 0; /* 1/4 概率生成"无元字符"的干净文本 */
for (uint32_t i = 0; i < n; i++) {
char c;
if (pure) {
do {
c = alpha[rnd_below((uint32_t)alen)];
} while (markdown_metachar((unsigned char)c) || c == '\0');
} else {
uint32_t mode = rnd_below(10);
if (mode < 7) c = alpha[rnd_below((uint32_t)alen)];
else if (mode < 9) c = (char)(0x20 + rnd_below(0x5F));
else c = (char)(rnd() & 0xFF); /* 偶尔来点非法 UTF-8 */
}
if (c == '\0') c = 'x';
sb_putc(&sb, c);
}
const char *src = sb.data ? sb.data : "";
size_t len = sb.len;
if (len == 0) { sb_free(&sb); return 0; }
char *whole = md_render(src, len, 0);
if (!whole) { sb_free(&sb); return 0; }
if (strlen(whole) > MD_RENDER_LIMIT * 4) {
snprintf(err, errlen, "输出异常膨胀:输入 %zu 字节 => %zu 字节",
len, strlen(whole));
free(whole); sb_free(&sb);
return 1;
}
/* 分片不变性 */
size_t chunks[4];
for (int k = 0; k < 4; k++) chunks[k] = 1 + rnd_below(8);
for (int k = 0; k < 4; k++) {
char *part = md_render(src, len, chunks[k]);
if (!part) continue;
if (strcmp(whole, part) != 0) {
snprintf(err, errlen,
"分片不变性被破坏(%zu 字节/片)",
chunks[k]);
/* 落盘,方便用 build/reduce 做最小化 */
FILE *fp = fopen("build/md_fail.bin", "wb");
if (fp) { fwrite(src, 1, len, fp); fclose(fp); }
free(part); free(whole); sb_free(&sb);
return 1;
}
free(part);
}
/* 无元字符 => 必须逐字节原样 */
if (pure) {
if (strcmp(whole, src) != 0) {
snprintf(err, errlen, "纯净文本被改写了:\n 输入 %.300s\n 输出 %.300s",
src, whole);
free(whole); sb_free(&sb);
return 1;
}
}
free(whole);
sb_free(&sb);
return 0;
}
/* ------------------------------------------------------------------ */
int main(int argc, char **argv)
{
long iters = (argc > 1) ? strtol(argv[1], NULL, 10) : 3000;
RNG = (argc > 2) ? strtoull(argv[2], NULL, 10) : 0x9E3779B97F4A7C15ULL;
if (!RNG) RNG = 1;
char err[512];
long json_fail = 0, md_fail = 0;
printf("模糊测试:JSON %ld 轮 + Markdown %ld 轮(种子 %llu)\n",
iters, iters, (unsigned long long)RNG);
for (long i = 0; i < iters; i++) {
err[0] = '\0';
if (fuzz_json_once(err, sizeof err)) {
printf(" ✗ JSON 第 %ld 轮失败:%s\n", i, err);
if (++json_fail >= 3) break;
}
}
printf(" JSON %ld 轮,%ld 次失败\n", iters, json_fail);
for (long i = 0; i < iters; i++) {
err[0] = '\0';
if (fuzz_md_once(err, sizeof err)) {
printf(" ✗ Markdown 第 %ld 轮失败:%s\n", i, err);
if (++md_fail >= 3) break;
}
}
printf(" Markdown %ld 轮,%ld 次失败\n", iters, md_fail);
if (json_fail || md_fail) {
printf("\n模糊测试发现 %ld 个问题\n", json_fail + md_fail);
return 1;
}
printf("\n模糊测试通过(无崩溃、无泄漏、不变量全部成立)\n");
return 0;
}