/* 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 #include #include #include 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; }