news 2026/9/20 14:50:50

Blender 损坏 .blend 文件测试集解析:invalid_blendfiles 的构造方法与防御机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Blender 损坏 .blend 文件测试集解析:invalid_blendfiles 的构造方法与防御机制
  • 图形学
  • 3D渲染
  • 桌面应用
  • 音视频

【免费下载链接】blender

Official mirror of Blender

项目地址:https://gitcode.com/gh_mirrors/bl/blender
点击查看免费下载

导读

tests/files/invalid_blendfiles/是 Blender 仓库中专门存放"已知损坏" .blend 文件的目录。本文以目录内的 README.md 为骨架,逐文件剖析六个损坏样本的成因、构造手法与底层原理,并结合 blender 读取器源码、SDNA 解析源码 与 回归测试脚本 验证每类损坏的检测路径。读完本文,你将理解 .blend 文件块(File Block)与 SDNA 结构描述在加载时的校验机制,掌握如何为 Blender 构造"坏文件"作为模糊测试或健壮性回归素材。

一、目录定位:为什么仓库要"故意"收藏坏文件

.blend是 Blender 的原生二进制格式,加载器需要同时解析文件头、文件块链表(BHead)与内嵌的 SDNA 结构描述。任何一环被篡改,都可能让重建逻辑越界读写,轻则崩溃,重则构成任意内存破坏。因此 Blender 专门在测试仓库中保留一批已知无效、且曾经触发过真实缺陷的文件,作为回归测试的"靶子"。

README 开篇即说明了该目录的定位:

This folder contains blendfiles that are known to be invalid (either from unknown reasons, or on purpose). This file gathers as much information as possible about each of them.

即:目录内文件或因未知原因损坏、或被人为故意改坏,README 则尽可能完整地记录每一个文件的来龙去脉。六个样本可分为两类:

类别文件触发缺陷
篡改 ID 文件码invalid_ca_la_id_code.blend未知原因写入异常 IDcode
损坏 SDNA/文件块invalid_sdna_struct_size.blend重建越过块末尾(堆缓冲区溢出)
invalid_block_count.blend重建越过块末尾(堆缓冲区溢出)
invalid_block_struct_index.blend越界读取 SDNA 结构数组(崩溃)
invalid_global_block.blend空指针解引用(崩溃)
invalid_window_workspace_hook.blend释放无效文件时解引用部分读取的窗口(崩溃)

其中后五个样本的"预期拒绝"行为被固定在 tests/python/bl_blendfile_versioning.py 中持续回归验证。

二、invalid_ca_la_id_code.blend:篡改写入侧 IDcode 的构造样本

该文件是从默认启动场景(factory startup)生成的,方法是在写出阶段改写CameraLamp两个数据块(data-block)的 ID 文件码(IDcode)。README 给出了完整补丁(作用于当时的 writefile.cc):

--- a/source/blender/blenloader/intern/writefile.cc +++ b/source/blender/blenloader/intern/writefile.cc @@ -1894,7 +1894,14 @@ void BLO_write_struct_list_by_name(BlendWriter *writer, const char *struct_name, void blo_write_id_struct(BlendWriter *writer, int struct_id, const void *id_address, const ID *id) { - writestruct_at_address_nr(writer->wd, GS(id->name), struct_id, 1, id_address, id); + int filecode = GS(id->name); + if (filecode == ID_CA) { + filecode = MAKE_ID2('!', '?'); + } + else if (filecode == ID_LA) { + filecode = BLEND_MAKE_ID('!', '?', 'L', 'A'); + } + writestruct_at_address_nr(writer->wd, filecode, struct_id, 1, id_address, id); }

关键点在于:补丁只替换了文件中的 IDcode,而struct_id(指向 SDNA 结构的索引)保持不变——即文件内容与 SDNA 描述"错位"。构造中用到的两个宏语义不同:

  • MAKE_ID2('!', '?'):生成一个 2 字符的 ID 文件码,用于 Camera(原本ID_CA);
  • BLEND_MAKE_ID('!', '?', 'L', 'A'):生成 4 字符的 ID 文件码,用于 Lamp(原本ID_LA)。

这种"文件码与结构类型不匹配"的样本,正是为了验证读取器遇到 IDcode 与 SDNA 结构不一致时能否安全拒绝,而不是将错就错地按错误类型重建数据块。从源码结构看,它也是六个样本中唯一由写入侧构造的,其余样本全部直接改动文件字节。

三、Corrupt SDNA:五个文件块级损坏样本

README 指出后五个样本的共性构造手法:

Factory-startup saves with a few bytes altered to corrupt the file header (DNA1) or a file-block header. Each crashed the reader before its fix; the expected rejection is checked intests/python/bl_blendfile_versioning.py.

即:以出厂启动场景保存的文件为基础,仅篡改少量字节,分别损坏文件头中的DNA1段或某个文件块头(file-block header)。修复前,每个样本都能让读取器崩溃;修复后,读取器必须稳定拒绝,且拒绝行为由测试脚本锁定。

3.1invalid_sdna_struct_size.blend:SDNA 结构尺寸自相矛盾

该样本将某个内嵌结构的TLEN(结构总长度字段)增大,使其不再等于各成员尺寸之和。README 记录的缺陷后果是重建过程越过块末尾,触发堆缓冲区溢出(heap-buffer-overflow)

读取器侧对这类损坏的拦截位于 dna_genfile.cc 的sdna_struct_size_check()

static bool sdna_struct_size_check(const SDNA *sdna, const SDNA_Struct *struct_info, const char **r_error_message) { int64_t members_size = 0; for (const SDNA_StructMember &member : Span(struct_info->members, struct_info->members_num)) { const StringRef member_name = sdna->members[member.member_index]; const int64_t array_num = sdna->members_array_num[member.member_index]; const int64_t member_size = ispointer(member_name) ? sdna->pointer_size : sdna->types_size[member.type_index]; members_size += member_size * array_num; } if (members_size != sdna->types_size[struct_info->type_index]) [[unlikely]] { *r_error_message = "Invalid struct size in SDNA file"; return false; } return true; }

该函数逐个成员累加尺寸(指针按pointer_size计、数组按array_num缩放),若与声明的结构总尺寸不一致,即返回错误信息"Invalid struct size in SDNA file"。函数注释点明了设计依据:Blender 自身的 DNA 永远满足"尺寸=成员之和"makesdna使用显式_pad成员而非隐式填充),因此一旦不匹配必然意味着尺寸被篡改——而尺寸被篡改会直接导致后续结构重建以错误步长越过文件数据末尾。

测试脚本 bl_blendfile_versioning.py 对该样本的预期是:

# invalid_blendfiles/invalid_sdna_struct_size.blend # A struct's DNA size no longer matches the sum of its members. "invalid_sdna_struct_size.blend": ( (OSError, RuntimeError), "Invalid struct size in SDNA file" ),

3.2invalid_block_count.blend:文件块元素计数越界

该样本把某个文件块的元素计数(nr)改为0x7fffffff。文件块头中的nr表示该块包含多少个结构实例,重建逻辑会以"结构尺寸 × nr"为步长遍历块内数据,于是这个天文数字直接让遍历越过块末尾,同样触发堆缓冲区溢出。

读取器在 readfile.cc 的read_struct()中做了防御:

/* The block must be large enough to hold the number of structs it claims, otherwise * reconstruction (which strides the block by the file struct size) reads past its end. * Written as a division to avoid overflow in `nr * struct_size`. */ const int64_t old_struct_size = DNA_struct_size(fd->filesdna.get(), bh->SDNAnr); if (bh->nr < 0 || (old_struct_size != 0 && bh->nr > bh->len / old_struct_size)) [[unlikely]] { fd->flags &= ~FD_FLAGS_FILE_OK; if (fd->bmain) { blo_readfile_invalidate(fd, fd->bmain, "Corrupt .blend file, invalid block count"); } return nullptr; }

注意实现细节:校验用除法bh->len / old_struct_size而非乘法比较,正是注释中强调的"避免nr * struct_size溢出";同时nr < 0的负数情况也被拦截。违反时通过blo_readfile_invalidate()使文件失效,错误信息为"Corrupt .blend file, invalid block count",测试脚本断言完全一致。

3.3invalid_block_struct_index.blend:SDNA 结构索引越界

该样本把文件块的 SDNA 结构索引(SDNAnr)改为0x40000000,远超文件内 SDNA 结构数组的长度,修复前读取器会越界读取 SDNA 结构数组并崩溃

read_struct()中对应的防护在索引使用之前就位:

/* A corrupt file could reference a struct index outside the file's SDNA, used below to index * the compare-flags, reconstruction & alignment tables. */ if (bh->SDNAnr < 0 || bh->SDNAnr >= fd->filesdna->structs.size()) [[unlikely]] { fd->flags &= ~FD_FLAGS_FILE_OK; if (fd->bmain) { blo_readfile_invalidate(fd, fd->bmain, "Corrupt .blend file, invalid block struct index"); } return nullptr; }

该索引随后会被用于访问 compare-flags、重建与对齐三张表,因此必须在任何索引操作前完成边界检查。测试脚本期望错误信息"Corrupt .blend file, invalid block struct index"

3.4invalid_global_block.blend:全局块(GLOB)上的同类损坏

该样本与invalid_block_struct_index.blend同构,但损坏点落在必需的全局(GLOB)块上:全局块同样引用了越界的 SDNA 结构索引。其独特之处在于缺陷形态不同——修复前,失败的读取返回null后被直接解引用,属于空指针解引用(crash)

测试脚本对它的期望错误信息为"is corrupt, unable to read",对应 readfile.cc 中:

BKE_reportf(reports, RPT_ERROR, "Blend file '%s' is corrupt, unable to read", fd->relabase);

全局块(GLOB)是文件加载流程中最早被强制读取的块之一,其内容决定文件基本参数的解析,因此它损坏时连常规的"块结构索引"错误路径都不再适用,直接以"文件损坏、无法读取"整体拒绝。

3.5invalid_window_workspace_hook.blend:窗口子块上的同类损坏

最后一个样本把同样的越界SDNAnr放到窗口(Window)的WorkSpaceInstanceHook子块上。README 记录的缺陷是:在释放(free)被判定无效的文件时,解引用了一个只被部分读取的窗口对象,导致崩溃。这说明了损坏样本的另一类价值——不仅读取路径要健壮,错误处理与资源清理路径(invalidate 之后的 free 流程)同样需要防护。

测试脚本对它期望的错误信息同样是"Corrupt .blend file, invalid block struct index",与 3.3 样本共用同一条检测分支,但覆盖的是完全不同的执行路径(窗口子块 vs 普通数据块)。

四、回归测试如何锁定这些"预期拒绝"

五个 Corrupt SDNA 样本的期望行为全部登记在 bl_blendfile_versioning.py 的invalid_paths字典中(该字典同时收录了其他目录下的损坏文件,如 Big-Endian 文件与各类已知坏文件)。其核心机制在invalid_path_exception_process()

def invalid_path_exception_process(self, bfp, exception): expected_failure = self.invalid_paths.get(os.path.basename(bfp), None) if not expected_failure: raise exception # Check expected exception type(s). if not isinstance(exception, expected_failure[0]): raise exception # Check expected exception (partial) message. if expected_failure[1] not in str(exception): raise exception print(f"\tExpected failure: '{exception}'", flush=True)

它同时校验两点:

  1. 异常类型:必须是元组(OSError, RuntimeError)之一(读取器以报告/异常形式拒绝);
  2. 异常消息(部分匹配):必须包含字典中登记的预期错误信息子串,如"Invalid struct size in SDNA file""Corrupt .blend file, invalid block count"等。

两条任一不满足都会让测试失败,从而确保:文件必须被拒绝、且必须以预期的具体理由被拒绝——既防止回归到"能打开坏文件"的错误行为,也防止"拒绝理由漂移"掩盖实现变更。

测试执行方式见脚本头部注释:

# ./blender.bin --background --python tests/python/bl_blendfile_versioning.py ..

该测试会递归扫描tests/files/下所有.blend,对每个文件执行打开(open)、链接(link)、追加(append)、另存重载(save & reload)与全局撤销(global undo)等操作;对登记在invalid_paths中的文件,上述任一操作抛出预期异常即视为通过。脚本还通过--slice-range/--slice-index支持并行分片处理(见generate_slice_indices()),以适应测试仓库中数千个.blend文件的规模。此外,脚本头部还留有一条来自开发者的警告:部分文件在 Linux/macOS 上会"看似随机"地失败,添加新文件需谨慎,这从侧面说明坏文件测试天然存在平台敏感性。

五、构造自己的损坏样本:方法与边界

综合 README 与源码,构造同类样本的通用流程是:

  1. 基础文件:以出厂启动场景保存一个干净的.blend(对应 README 所述 "Factory-startup saves");
  2. 定位目标:用十六进制编辑器定位DNA1段(文件头中描述 SDNA 的区块)或目标文件块(BHead)头;
  3. 定向篡改
    • TLEN(SDNA 结构长度)→ 复现invalid_sdna_struct_size
    • 改文件块头的nr(元素计数)→ 复现invalid_block_count
    • 改文件块头的SDNAnr(SDNA 结构索引)→ 复现invalid_block_struct_index,改在GLOB块上即复现invalid_global_block,改在窗口的WorkSpaceInstanceHook子块上即复现invalid_window_workspace_hook
  4. 验证拒绝:在bl_blendfile_versioning.py中登记文件名与预期错误信息后运行测试,或在命令行手动打开确认报错信息。

需要说明的边界:README 对invalid_ca_la_id_code.blend仅注明"unknown reasons, or on purpose",未在测试脚本中登记预期拒绝信息,说明该文件当前的失败原因尚未完全明确(脚本顶部警告"部分文件看似随机失败"或与此类不确定性相关);其余五个样本的成因、缺陷形态与检测路径则均已通过源码与测试双重确认。

六、小结:一份"坏文件"档案的工程价值

tests/files/invalid_blendfiles/及其 README 展示了 Blender 在文件格式健壮性上的两个工程实践:

  • 以真实缺陷为素材:每个坏文件都对应一个曾经真实发生的崩溃(堆缓冲区溢出、越界读取、空指针解引用、释放路径解引用),比随机模糊数据更能精准覆盖回归点;
  • 以文档+测试双重锁定:README 记录"怎么改坏的、会触发什么缺陷",bl_blendfile_versioning.py 记录"必须报什么错",而 readfile.cc 与 dna_genfile.cc 则提供拦截这些损坏的具体实现——三者共同构成"构造 → 修复 → 回归"的完整闭环。

对于任何需要解析 Blender 文件格式或构建健壮二进制解析器的开发者,这份档案既是格式内部结构的反面教材,也是验证自身解析器边界检查是否完备的现成测试集。

  • 图形学
  • 3D渲染
  • 桌面应用
  • 音视频

【免费下载链接】blender

Official mirror of Blender

项目地址:https://gitcode.com/gh_mirrors/bl/blender
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 14:49:06

BrewUI:用图形界面拯救被命令行劝退的Homebrew用户

1. 从命令行到图形界面&#xff1a;BrewUI 到底是什么1.1 被命令行劝退的瞬间我先说个实话&#xff0c;在 macOS 上做开发的人&#xff0c;几乎没有不知道 Homebrew 的。这个包管理器解决了我日常装软件、装依赖、管理版本的一大半痛点&#xff0c;一条brew install打天下&…

作者头像 李华
网站建设 2026/9/20 14:48:43

GPT-4技术报告深度解读:从原理到实战的全面拆解

简介&#xff1a;这份PDF提供OpenAI《GPT-4技术报告》的中文翻译&#xff0c;面向想系统了解GPT-4多模态架构、能力边界与安全问题的研究者、开发者和AI爱好者。内容覆盖GPT-4在律师考试、MMLU等基准上的表现&#xff0c;讲解Transformer预训练、RLHF调优、可预测扩展方法&…

作者头像 李华
网站建设 2026/9/20 14:46:19

运维效率分水岭:Cmd命令实战手册与跨平台命令对照

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 14:45:36

STM32+ESP8266+OneNet智能家居实战:MQTT协议从硬件到云端全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 14:45:06

微信占满C盘?三步迁移文件+清理缓存,彻底释放空间

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 14:44:04

GetQzonehistory:扫码一次,把QQ空间历史说说全部导出成Excel

GetQzonehistory&#xff1a;扫码一次&#xff0c;把QQ空间历史说说全部导出成Excel 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 想把QQ空间的历史说说导出来&#xff1f;网页版翻到…

作者头像 李华