Python C扩展安全测试:Fuzzing+ASan+UBSan实战指南

发布时间:2026/5/22 7:45:30

Python C扩展安全测试:Fuzzing+ASan+UBSan实战指南 1. 这不是演习当CVE-2024-XXXX的倒计时在监控面板上跳动时你手里的Python扩展模块就是最后一道防线凌晨两点十七分我盯着屏幕上那个不断跳动的红色数字——72:00:00。这不是某个CTF比赛的计时器而是客户生产环境里一个用Cython编译的图像处理扩展模块所暴露的CVE-2024-XXXX漏洞的SLA修复窗口。它被标记为“Critical”CVSS评分9.8触发条件是用户上传一张特制的PNG文件就能绕过所有Python层的输入校验直接在C层触发堆缓冲区溢出进而执行任意代码。而这个模块正运行在每天处理3700万张图片的AI标注流水线上。你可能觉得奇怪Python不是以“安全”著称吗GIL、内存自动管理、类型动态检查……怎么还会冒出这种底层C级的高危漏洞答案就藏在标题里的三个词里Python扩展模块。它不是纯Python代码而是用C/C/Rust写的原生二进制通过CPython C API与Python解释器桥接。Python层的防护再严密也挡不住C层一个越界的memcpy。这就像给一栋玻璃幕墙大楼装了最顶级的门禁系统却忘了检查承重墙里埋着的、早已锈蚀的钢筋。所以这篇指南不讲“为什么安全很重要”也不教你怎么写一个Hello World扩展。它是一份72小时倒计时下的作战手册一份专为Python C扩展包括Cython、pybind11、cffi封装的C库量身定制的紧急安全测试清单。它聚焦三个核心武器Fuzzing模糊测试——用海量畸形输入主动“撞门”ASanAddressSanitizer——像给内存装上高清摄像头实时捕捉每一次非法读写UBSanUndefinedBehaviorSanitizer——专门揪出那些C标准里明令禁止、但编译器默许执行的危险操作比如有符号整数溢出、空指针解引用、未初始化变量使用。这三者不是并列选项而是必须串联使用的“三位一体”组合Fuzzing提供弹药ASan/UBSan提供瞄准镜和扳机缺一不可。如果你正在维护一个用C/C写的Python扩展或者你的团队刚接手了一个历史遗留的、文档稀少的.so/.dll模块又或者你正准备将一个关键算法从Python重写为C以提升性能——那么你现在打开这篇文章就是对生产环境最务实的守护。它不假设你精通LLVM编译原理但要求你愿意在终端里敲几行命令它不承诺能100%发现所有漏洞但能确保你在72小时内把最致命、最容易被利用的那批缺陷从黑暗中拖到光下。2. 为什么是FuzzingASanUBSan拆解这套组合拳的底层逻辑与不可替代性在开始敲命令之前我们必须回答一个根本问题为什么是这三个工具为什么不能只用其中一个为什么不能用更“高级”的静态分析或SAST工具这个问题的答案决定了你是在做一场有章法的攻防演练还是在盲目地撒网捕鱼。2.1 Fuzzing不是随机乱试而是有目标的“压力爆破”很多人对Fuzzing的第一印象是“随机生成输入”。这没错但远远不够。对于Python扩展真正的Fuzzing是基于接口契约的定向爆破。你的扩展模块对外暴露的通常是一个或几个Python函数比如image_processor.decode_png(raw_bytes: bytes) - dict。Fuzzing的目标就是围绕这个函数签名系统性地生成成千上万种raw_bytes的变体去试探它的边界。这里的关键在于“变异策略”。一个简单的随机字节流大概率连函数入口都进不去就会被Python层的参数类型检查bytes类型校验或基础长度校验比如len(raw_bytes) 4就直接返回错误给拦下来。真正有效的Fuzzing必须理解这个函数的隐式协议。PNG文件有固定的魔数89 50 4E 47 0D 0A 1A 0A有特定的块结构IHDR、IDAT、IEND。一个专业的Fuzzer如libFuzzer它被集成在Clang/LLVM中会先学习这些结构然后在合法结构的框架内精准地变异关键字段比如把IHDR块里的宽度字段从0x00000100256变异为0xFFFFFFFF4294967295看看C层的malloc(width * height * 4)会不会崩溃或者把IDAT块的压缩数据末尾强行截断制造一个不完整的zlib流看解压函数是否会越界读取。提示Fuzzing的价值不在于它能发现多少个漏洞而在于它能在极短时间内覆盖人类思维难以穷尽的、最危险的输入组合。一个经验丰富的C程序员可能会想到“传入超大尺寸”但很难系统性地想到“传入一个宽度为0、高度为0xFFFFFFFF的PNG”而Fuzzer会。2.2 ASan内存安全的“X光机”让所有越界行为无所遁形假设Fuzzing成功触发了一个崩溃。接下来的问题是崩溃在哪里发生的是decode_png函数里还是它调用的某个第三方PNG库如libpng里是栈溢出、堆溢出还是使用了已经free掉的内存Use-After-Free这时候ASan就登场了。它不是一个调试器而是一个编译时注入的、运行时的内存监视器。当你用-fsanitizeaddress编译你的C扩展时Clang会在你的所有内存操作malloc,free,memcpy,strcpy等前后插入大量的检查代码。它会为你的整个进程分配一块巨大的“影子内存”Shadow Memory用来实时记录每一块真实内存的“状态”这块内存是否已分配是否已释放它的左右边界在哪里当Fuzzer送入一个恶意输入导致memcpy(dst, src, len)中的len远超dst的容量时ASan的检查代码会在memcpy执行前查询dst的影子内存。它立刻发现dst指向的内存块其合法长度只有1024字节而你却要拷贝1000000字节。于是ASan会立即中断程序并打印出一份极其详尽的报告 12345ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000eff0 at pc 0x7f8b1a2c3d4e bp 0x7ffce3a2b1e0 sp 0x7ffce3a2b1d8 WRITE of size 4 at 0x60200000eff0 thread T0 #0 0x7f8b1a2c3d4d in decode_png /path/to/module.c:142 #1 0x7f8b1a2c4abc in PyDecodePngFunc /path/to/module.c:201 #2 0x55a1b2c3d4ef in _PyMethodDef_RawFastCallKeywords ... ... 0x60200000eff0 is located 0 bytes to the right of 1024-byte region [0x60200000ebe0,0x60200000eff0) allocated by thread T0 here: #0 0x7f8b1a5d4a12 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.50x10ca12) #1 0x7f8b1a2c3a01 in decode_png /path/to/module.c:135 ...这份报告精确到行号module.c:142清晰地告诉你崩溃发生在decode_png函数的第142行是一次对堆内存的越界写入heap-buffer-overflow并且指出了这块内存是在第135行被分配的。这比GDB单步调试快一百倍因为它不需要你去猜、去设断点它直接把根因钉死在源码上。2.3 UBSan捕获C语言的“灰色地带”堵住ASan也看不见的漏洞ASan能抓到内存越界但它对另一类同样致命的漏洞却无能为力未定义行为Undefined Behavior, UB。C标准规定当程序执行到某些操作时其结果是“未定义”的这意味着编译器可以自由选择任何行为——忽略、崩溃、产生错误结果甚至什么也不做。UBSan就是专门来捕获这些“灰色地带”的。最常见的UB包括有符号整数溢出int a INT_MAX; a;在C中是UB但在很多平台上它只是简单地变成INT_MIN二进制翻转程序继续运行但结果完全错误。如果这个溢出被用来计算内存分配大小size_t size width * height * 4;那后果就是灾难性的。空指针解引用int *p NULL; *p 1;这在ASan里会被捕获为SEGV但UBSan能更早地在p被声明为NULL后第一次被解引用时就报警。未初始化变量使用int x; return x * 2;x的值是随机的可能导致后续逻辑分支错误。UBSan的工作方式与ASan类似也是编译时注入检查。当你加上-fsanitizeundefinedClang会在每个潜在UB发生的地方插入检查。例如对于a b它会检查a和b相加是否会溢出。一旦检测到它会打印类似这样的信息/path/to/module.c:88:15: runtime error: signed integer overflow: 2147483647 1 cannot be represented in type int这个错误信息直接指向了问题的根源一个有符号整数溢出。它比ASan的崩溃报告更“上游”因为它发生在错误结果被用于危险操作如malloc之前让你有机会在漏洞被利用前就将其扼杀。注意ASan和UBSan可以同时启用即-fsanitizeaddress,undefined。它们的检查是正交的互不干扰共同构成一张严密的安全监控网。3. 实战部署从零开始构建你的Python扩展安全测试环境含完整命令与配置现在我们把理论付诸实践。以下步骤是我过去三年在多个项目中反复验证过的、最精简高效的部署流程。它不依赖任何云服务或复杂CI/CD只需要一台干净的Linux开发机Ubuntu 22.04 LTS或CentOS 8就能在30分钟内跑通整个链条。3.1 环境准备安装现代Clang与Python开发套件首先确保你有一个支持ASan/UBSan的现代编译器。GCC 8也支持但Clang的错误报告更友好、更详细是我们的首选。# Ubuntu/Debian sudo apt update sudo apt install -y clang-14 libc-14-dev libcabi-14-dev python3-dev python3-pip # CentOS/RHEL (启用PowerTools/EPEL) sudo dnf install -y clang llvm-toolset python3-devel关键点在于python3-dev或python3-devel。它提供了Python.h头文件和libpython3.x.so库这是编译任何Python扩展的基石。没有它你的setup.py会报错fatal error: Python.h: No such file or directory。接着安装pytest和pytest-forked后者允许我们在独立进程中运行每个Fuzz测试用例避免一个崩溃导致整个测试套件退出。pip3 install pytest pytest-forked3.2 编译你的扩展模块注入ASan与UBSan这是最关键的一步。你需要修改你的扩展构建脚本。无论你用的是setup.pydistutils/setuptools、pyproject.tomlmodern setuptools还是meson.build核心都是向C编译器传递正确的-fsanitize标志。方案A如果你用setup.py最常见找到你的setup.py文件在Extension对象的定义中添加extra_compile_args和extra_link_argsfrom setuptools import setup, Extension import sys # 检测是否在进行安全测试 SANITIZE True if --sanitize in sys.argv else False ext_modules [ Extension( my_extension, # 模块名 sources[src/my_extension.c], # 关键仅在SANITIZE模式下注入Sanitizer extra_compile_args[-O1, -g, -fsanitizeaddress,undefined, -fno-omit-frame-pointer] if SANITIZE else [-O2, -g], extra_link_args[-fsanitizeaddress,undefined] if SANITIZE else [], # 如果你的模块链接了外部C库如libpng也需要对它们启用ASan # libraries[png], # library_dirs[/usr/lib/x86_64-linux-gnu], ) ] setup( namemy-extension, ext_modulesext_modules, # 其他参数... )然后用特殊命令编译# 清理旧的构建 rm -rf build/ my_extension.egg-info/ # 使用--sanitize标志进行编译 python3 setup.py build_ext --inplace --sanitize # 验证编译是否成功并检查是否包含了Sanitizer ldd ./my_extension.cpython-*.so | grep asan # 应该输出类似libasan.so.5 /usr/lib/llvm-14/lib/libasan.so.5方案B如果你用pyproject.toml推荐在pyproject.toml中你可以利用setuptools的build_ext配置[build-system] requires [setuptools45, wheel, setuptools_scm[toml]6.2] [project] name my-extension # ... 其他配置 [project.optional-dependencies] dev [pytest, pytest-forked] [tool.setuptools] # 启用动态扩展构建 # ... [tool.setuptools.build-ext] # 为C扩展指定编译和链接参数 # 注意这里需要根据你的实际需求调整 compile-args [-O1, -g, -fsanitizeaddress,undefined, -fno-omit-frame-pointer] link-args [-fsanitizeaddress,undefined]然后编译pip3 install -e .[dev] --config-settings editable-verbosetrue经验心得我强烈建议在setup.py或pyproject.toml中加入一个--sanitize开关而不是永久启用。因为ASan/UBSan会让程序慢5-10倍内存占用翻倍只应在测试阶段启用。生产环境务必使用-O2或-O3优化编译。3.3 编写第一个Fuzz Target将Python函数转化为libFuzzer入口libFuzzer是一个“in-process”Fuzzer它需要一个C/C函数作为入口点。我们的任务就是把这个入口点精准地对接到你的Python扩展函数上。假设你的扩展里有一个核心函数decode_png它接收一个PyBytesObject*返回一个PyObject*。我们需要创建一个fuzz_target.cc文件// fuzz_target.cc #include cstdint #include cstddef #include Python.h // 声明你的扩展模块的C函数通常在module.c里定义 extern C { // 这是你在module.c里定义的、被Python调用的C函数 // 它的签名通常是PyObject* PyDecodePngFunc(PyObject* self, PyObject* args) extern PyObject* PyDecodePngFunc(PyObject* self, PyObject* args); } // libFuzzer的入口函数 extern C int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { // 1. 初始化Python解释器仅在首次调用时 static bool py_initialized false; if (!py_initialized) { Py_Initialize(); // 加载你的扩展模块假设模块名为my_extension PyRun_SimpleString(import my_extension); py_initialized true; } // 2. 将libFuzzer提供的原始字节数组包装成Python bytes对象 PyObject* py_bytes PyBytes_FromStringAndSize(reinterpret_castconst char*(data), size); if (!py_bytes) { return 0; // 创建失败跳过 } // 3. 构造一个Python tuple模拟函数调用的参数(py_bytes,) PyObject* args PyTuple_New(1); PyTuple_SET_ITEM(args, 0, py_bytes); // 注意SET_ITEM会接管py_bytes的引用计数 // 4. 调用你的Python函数这会间接调用PyDecodePngFunc // 这里假设你的模块有一个顶层函数叫decode_png PyObject* result PyObject_CallObject(PyImport_AddModule(my_extension), args); // 5. 清理 Py_DECREF(args); if (result) { Py_DECREF(result); } // 返回0表示本次Fuzz成功libFuzzer可以继续 return 0; }编译这个Fuzz Target# 将你的扩展模块和Fuzz Target一起编译 clang -fsanitizeaddress,undefined -fno-omit-frame-pointer \ -I/usr/include/python3.10 -I./build/lib.linux-x86_64-cpython-310 \ -L./build/lib.linux-x86_64-cpython-310 -lmy_extension \ -lpython3.10 -lpthread -ldl -lutil -lm \ -o fuzz_decode_png fuzz_target.cc这个命令很长但每一部分都至关重要-I指定了Python头文件和你本地编译的扩展模块的路径。-L和-lmy_extension告诉链接器去哪里找你的.so文件。-lpython3.10是链接Python解释器本身。3.4 运行Fuzzing启动、监控与结果解读一切就绪现在启动Fuzzer# 创建一个语料库目录存放初始的“好”样本 mkdir -p corpus/ # 放入一个合法的PNG文件作为种子 cp test_images/good.png corpus/ # 开始Fuzzing ./fuzz_decode_png -max_total_time3600 corpus/ -jobs0 -workers4参数说明-max_total_time3600运行1小时3600秒这是72小时修复窗口里你应投入的首批“火力侦察”时间。corpus/语料库目录Fuzzer会在这里存放新发现的、能触发新代码路径的输入。-jobs0 -workers4使用4个CPU核心并行Fuzz。如何解读Fuzzer的输出当你看到类似这样的日志就意味着有重大发现INFO: Seed: 123456789 INFO: Loaded 1 modules (123456 inline 8-bit counters): 123456 [0x55a1b2c3d000, 0x55a1b2d3e000), INFO: Loaded 1 PC tables (123456 PCs): 123456 [0x55a1b2d3e000,0x55a1b2e3f000), INFO: -max_len is not provided; libFuzzer will use 64 INFO: A corpus is not provided; starting from an empty corpus #0 READ units: 1 #1 INITED cov: 1234 ft: 5678 corp: 1/1b exec/s: 0 rss: 45Mb #2 NEW cov: 1235 ft: 5679 corp: 2/2b lim: 64 exec/s: 0 rss: 45Mb L: 1/1 MS: 1 CopyPart- #1000 NEW cov: 1245 ft: 5789 corp: 10/100b lim: 64 exec/s: 123 rss: 48Mb L: 10/10 MS: 2 ShuffleBytes-CopyPart- ... #10000 pulse cov: 1255 ft: 5890 corp: 15/150b lim: 64 exec/s: 110 rss: 52Mb #10001 REDUCE cov: 1255 ft: 5890 corp: 15/145b lim: 64 exec/s: 110 rss: 52Mb L: 5/10 MS: 1 EraseBytes- #10002 NEW cov: 1256 ft: 5891 corp: 16/148b lim: 64 exec/s: 110 rss: 52Mb L: 3/10 MS: 1 EraseBytes- ... #20000 pulse cov: 1260 ft: 5900 corp: 18/160b lim: 64 exec/s: 105 rss: 55Mb #20001 REDUCE cov: 1260 ft: 5900 corp: 18/155b lim: 64 exec/s: 105 rss: 55Mb L: 5/10 MS: 1 EraseBytes- #20002 NEW cov: 1261 ft: 5901 corp: 19/158b lim: 64 exec/s: 105 rss: 55Mb L: 3/10 MS: 1 EraseBytes- ... #30000 pulse cov: 1265 ft: 5910 corp: 20/170b lim: 64 exec/s: 100 rss: 58Mb #30001 REDUCE cov: 1265 ft: 5910 corp: 20/165b lim: 64 exec/s: 100 rss: 58Mb L: 5/10 MS: 1 EraseBytes- #30002 NEW cov: 1266 ft: 5911 corp: 21/168b lim: 64 exec/s: 100 rss: 58Mb L: 3/10 MS: 1 EraseBytes- ... #36000 pulse cov: 1270 ft: 5920 corp: 22/180b lim: 64 exec/s: 95 rss: 60Mb #36001 REDUCE cov: 1270 ft: 5920 corp: 22/175b lim: 64 exec/s: 95 rss: 60Mb L: 5/10 MS: 1 EraseBytes- #36002 NEW cov: 1271 ft: 5921 corp: 23/178b lim: 64 exec/s: 95 rss: 60Mb L: 3/10 MS: 1 EraseBytes- #36003 DONE exec/s: 95 rss: 60Mb这里的关键词是NEW和REDUCE。NEW表示Fuzzer发现了一个能触发新代码路径ft: 5921比之前的5920多了一个的输入这是一个好信号说明Fuzzer在有效探索。REDUCE表示它在尝试最小化这个输入去掉冗余字节让它更“干净”。最重要的时刻当Fuzzer崩溃时如果Fuzzer发现了漏洞它会停止并打印一个详细的ASan/UBSan报告。这个报告就是你的“战利品”。把它复制下来保存为crash-xxxxxx文件这就是你修复工作的起点。4. 从崩溃报告到代码修复一份真实的漏洞排查与修复全流程复盘理论和工具都已就位现在进入最紧张、也最有价值的部分实战排雷。下面我将以一个真实案例——CVE-2024-XXXX的前身一个在某医疗影像处理模块中发现的漏洞——来完整复盘从Fuzzer崩溃到最终代码修复的全过程。这个过程就是你在72小时内必须走完的路。4.1 捕获崩溃一份典型的ASan报告及其关键信息提取Fuzzer运行了约45分钟后突然中断并输出了如下报告 12345ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000eff0 at pc 0x7f8b1a2c3d4e bp 0x7ffce3a2b1e0 sp 0x7ffce3a2b1d8 WRITE of size 4 at 0x60200000eff0 thread T0 #0 0x7f8b1a2c3d4d in decode_png /home/dev/src/image_processor.c:142 #1 0x7f8b1a2c4abc in PyDecodePngFunc /home/dev/src/image_processor.c:201 #2 0x55a1b2c3d4ef in _PyMethodDef_RawFastCallKeywords ... ... 0x60200000eff0 is located 0 bytes to the right of 1024-byte region [0x60200000ebe0,0x60200000eff0) allocated by thread T0 here: #0 0x7f8b1a5d4a12 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.50x10ca12) #1 0x7f8b1a2c3a01 in decode_png /home/dev/src/image_processor.c:135 ... SUMMARY: AddressSanitizer: heap-buffer-overflow /home/dev/src/image_processor.c:142 in decode_png第一步信息提取错误类型heap-buffer-overflow—— 堆缓冲区溢出高危。发生位置image_processor.c文件的第142行函数decode_png。分配位置同一文件的第135行malloc调用。溢出偏移0x60200000eff0是溢出地址它位于分配的1024字节区域的末尾之后0字节意味着是恰好越界写入1个字节。4.2 定位代码聚焦第135行与第142行我们立刻打开image_processor.c定位到第135行和第142行// image_processor.c // 第135行分配内存 uint8_t* pixel_data (uint8_t*) malloc(width * height * 4); // RGBA, 4 bytes per pixel // ... 中间省略大量PNG解析逻辑 ... // 第142行写入像素数据 pixel_data[(y * width x) * 4 0] r; // R pixel_data[(y * width x) * 4 1] g; // G pixel_data[(y * width x) * 4 2] b; // B pixel_data[(y * width x) * 4 3] a; // A问题看起来非常直观width * height * 4的计算结果就是分配的内存大小。而第142行的索引(y * width x) * 4 3必须小于这个大小否则就会越界。4.3 深度根因分析为什么width * height * 4会溢出如果只是width和height很大比如10000 * 10000 * 4 400,000,000这在64位系统上是完全OK的。但ASan报告说只分配了1024字节。这说明width * height * 4的计算结果在乘法过程中就发生了整数溢出变成了一个很小的数。我们回到PNG解析部分找到了width和height的来源// 从PNG IHDR chunk中读取 uint32_t width png_read_uint32(ihdr_data 4); // IHDR offset 4-7 uint32_t height png_read_uint32(ihdr_data 8); // IHDR offset 8-11png_read_uint32函数返回的是uint32_t一个无符号32位整数。问题就出在这里width * height * 4这个表达式在C语言中其类型是由操作数决定的。width和height是uint32_t4是int。根据C的整型提升规则整个表达式会被提升为uint32_t。而uint32_t的最大值是4294967295。当width 0x800000002147483648且height 2时width * height 0x100000000这已经超出了uint32_t的范围会发生回绕wrap-around结果变成0。0 * 4 0所以malloc(0)被调用它返回了一个最小的、非NULL的指针通常是16字节对齐的地址而我们的代码把它当成了一个1024字节的缓冲区来用。这就是一个典型的无符号整数溢出Unsigned Integer Overflow它本身在C标准中不是UB它是定义好的回绕行为但它直接导致了后续的堆溢出。UBSan对此无能为力但ASan完美地捕获了最终的后果。4.4 修复方案四层防御体系的构建一个合格的修复绝不仅仅是把malloc改成calloc或者加个if判断。它必须是纵深防御的。我们采用了四层修复第一层输入校验最外层在从PNG IHDR读取width和height后立即进行业务逻辑校验// 在读取width和height之后 if (width 0 || height 0 || width 16384 || height 16384) { PyErr_SetString(PyExc_ValueError, Invalid PNG dimensions); return NULL; }16384是一个合理的上限远大于任何真实场景的图片尺寸但又足够小能防止width * height在uint32_t范围内溢出16384 * 16384 268,435,456远小于4294967295。第二层安全的算术运算中间层使用__builtin_mul_overflowClang/GCC内置函数来安全地计算乘积size_t size; if (__builtin_mul_overflow((size_t)width, (size_t)height, size) || __builtin_mul_overflow(size, (size_t)4, size)) { PyErr_SetString(PyExc_MemoryError, Image dimensions too large); return NULL; } uint8_t* pixel_data (uint8_t*) malloc(size); if (!pixel_data) { PyErr_NoMemory(); return NULL; }__builtin_mul_overflow会返回true如果乘法溢出并将结果存入第三个参数。这是最可靠、最高效的防溢出手段。第三层边界检查最内层在每次写入像素前增加一次运行时检查size_t idx (size_t)y * width x; if (idx (size_t)width * (size_t)height) { // 这个检查理论上不应该触发因为上面两层已经保证了 // 但它是一个最后的保险丝 PyErr_SetString(PyExc_RuntimeError, Internal pixel index error); free(pixel_data); return NULL; } pixel_data[idx * 4 0] r; // ... 其他写入第四层自动化回归测试持续层将触发崩溃的那个恶意PNG文件crash-xxxxxx放入你的测试语料库并编写一个pytest用例确保它能被正确捕获并抛出ValueError而不是导致进程崩溃# test_security.py def test_cve_2024_xxxx(): Regression test for CVE-2024-XXXX with open(test/crashes/crash-xxxxxx, rb) as f: bad_bytes f.read() with pytest.raises(ValueError, matchInvalid PNG dimensions): my_extension.decode_png(bad_bytes)经验心得我曾经在一个项目中只做了第一层校验认为“够用了”。结果一个月后另一个Fuzzer用不同的变异策略

相关新闻