首页 / 资讯中心 / 文章详情

深入解析 zstd 解压器勘误表:6 类合法帧被拒绝的边界缺陷与修复实践

深入解析 zstd 解压器勘误表:6 类合法帧被拒绝的边界缺陷与修复实践 ★ FEATURED ARTICLE
深入解析 zstd 解压器勘误表6 类合法帧被拒绝的边界缺陷与修复实践【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit本文以 zstd 官方decompressor_errata.md勘误文档为骨架结合本仓库中 zstd 1.5.7 的完整源码勘误文档、压缩器实现、解压器实现、回归测试逐条剖析 6 类被解压器错误拒绝的合法 zstd 帧每条缺陷的受影响版本、影响组件、复现帧、根因与修复状态并给出可操作的规避与数据恢复方案帮助读者理解 zstd 帧格式规范边界避免在自研或二次开发解压器时重蹈覆辙。zstdZstandard在追求极致压缩比的同时其帧格式规范也在不断演进这导致历史上部分合法帧曾被解压器错误拒绝。本仓库fluent-bit 项目内置了 zstd 1.5.7 用于压缩与解压完整保留了这份勘误记录与其回归测试资产。本文按时间倒序逐条展开每条均给出可复现的十六进制示例帧并结合本仓库源码说明根因与当前修复状态最后汇总为一张速查表供开发者在排查解压失败但帧明明合法类问题时直接对照。勘误文档的价值与阅读方式zstd 的 decompressor_errata.md 是一份非常罕见的缺陷档案它记录的不是解压器拒绝损坏数据这是正确行为而是解压器拒绝了规范允许的合法帧——即decoder rejects a valid zstd frame。这类缺陷比普通 bug 更隐蔽因为帧本身完全符合 zstd 帧格式规范任何第三方压缩器都可能产出修复往往要同时改动解压路径并给压缩路径加规避补丁防止参考压缩器再产出此类帧复现需要精心构造的黄金样例帧golden file普通模糊测试很难命中。文档为每个条目固定提供 5 项信息本仓库中的对应资产可以直接验证最后受影响版本Last affected version受影响的解压组件Library / CLI 或两者参考压缩器是否可能产出该帧Produced by the reference compressor示例帧短帧直接给出十六进制串长帧指向黄金文件缺陷描述与根因。对应地仓库中 tests/golden-decompression 目录保存了 4 个黄金样例帧zeroSeq_2B.zst、block-128k.zst、empty-block.zst、rle-first-block.zstplayTests.sh 中的decompression only tests小节在每次 CI 时都会用当前解压器解压这些帧确保缺陷不会回归。缺陷 12 字节格式编码的 0 序列v1.5.5 修复最后受影响版本v1.5.5受影响组件Library 与 CLI参考压缩器是否产出否示例帧tests/golden-decompression/zeroSeq_2B.zst缺陷现象zstd 帧中每个压缩块的序列段Sequences section开头是Number_of_Sequences字段。该字段有两种编码格式1 字节格式字节值小于等于 0x7F 时直接表示序列数量2 字节格式字节值大于 0x7F 时与后续一个字节组合成一个 15 位的数值zstd_decompress_block.c 中ZSTD_decodeSeqHeaders()的解析逻辑。本缺陷正是发生在 2 字节格式的取值边界上当块内实际有 0 条序列且 0 恰好用 2 字节格式编码时旧版解压器会错误地继续期待 FSE 表而正确的行为应当是立即结束序列段、进入下一个块。根因分析对照当前 zstd_decompress_block.c 中的修复后逻辑if (nbSeq 0) { /* No sequence : section ends immediately */ RETURN_ERROR_IF(ip ! iend, corruption_detected, extraneous data present in the Sequences section); return (size_t)(ip - istart); }即解析出nbSeq 0后序列段必须立即结束同时还要校验序列段内不存在多余字节extraneous data。而 v1.5.5 及更早版本只在 1 字节格式解析出 0 时走这条短路路径2 字节格式解析出的 0 会错误地落入继续解析 FSE 表描述符的分支。为何参考压缩器从不产出文档明确指出参考压缩器从未产出过这类帧原因很直接用 2 字节格式表示 0 条序列是低效的——既然 1 字节格式就能表示 0压缩器永远优先使用 1 字节格式。这也是此类缺陷能存活多年未被发现的根本原因只有追求极端的第三方压缩器或手工构造帧才会踩中。仓库回归测试playTests.sh 用如下命令持续守护该场景zstd -t $TESTDIR/golden-decompression/zeroSeq_2B.zst同时 golden.sh 会对整个黄金目录执行zstd -r -t批量测试反向的 detectErrors.sh 则验证损坏帧必须被正确拒绝例如 playTests.sh 中的zeroSeq_extraneous.zst携带多余字节必须报错一正一反共同锁死行为边界。缺陷 2大小恰为 128 KB 的压缩块v1.5.2 修复最后受影响版本v1.5.2受影响组件Library 与 CLI参考压缩器是否产出否示例帧tests/golden-decompression/block-128k.zst缺陷现象zstd 解码器曾经错误地拒绝大小恰好为 128 KB131072 字节的Compressed_Block类型块。注意这个边界非常刁钻128 KB - 1的块被正常接受128 KB 1的块被规范明确禁止超出ZSTD_BLOCKSIZE_MAX唯独恰好128 KB的块被旧版解压器当作非法拒绝。在 zstd_compress.c 中压缩器侧有明确的规避注释与断言/* libzstd decoder before v1.5.4 is not compatible with * compressed blocks of size ZSTD_BLOCKSIZE_MAX exactly. */ assert(cSize ZSTD_BLOCKSIZE_MAX);根因与规范变迁zstd 帧格式规范zstd_compression_format.md在0.3.2 版本之前对Compressed_Block有一条额外限制A Compressed_Block has the extra restriction that Block_Size is always strictly less than the decompressed size. If this condition cannot be respected, the block must be sent uncompressed instead (Raw_Block).这条限制意味着压缩块大小必须严格小于解压后大小若不满足则必须改用未压缩的Raw_Block发送。旧版解压器正是基于这条过期限制拒绝 128 KB 的压缩块。该限制在规范 0.3.2 时被解除解压器直到 v1.5.2 才同步修复。文档引用的解除依据是 upstream 的 PR#1689该链接为外部引用仅作背景说明。复现与验证由于参考压缩器从不主动产出恰好 128 KB 的压缩块仓库通过黄金文件 block-128k.zst 固化复现用例。对关心此边界的开发者结论是只要解压器版本 ≥ v1.5.2恰好 128 KB 的压缩块即可正常解码。缺陷 30 字面量 0 序列的压缩块v1.5.2 修复最后受影响版本v1.5.2受影响组件Library 与 CLI参考压缩器是否产出否示例帧十六进制28b5 2ffd 2000 1500 0000 00缺陷现象zstd 解码器曾经错误地拒绝这样一类Compressed_Block其字面量部分以Raw_Literals_Block未压缩字面量块形式编码且字面量为 0 个同时序列数也为 0。换言之这是一个空转的压缩块不携带任何字面量、不携带任何序列却仍然被标记为压缩块类型。根因与缺陷 2 同源这类块同样触发了规范 0.3.2 之前的那条过期限制Block_Size必须严格小于解压后大小。一个 0 字面量 0 序列的压缩块其解压后大小为零显然无法满足严格小于关系旧版解压器据此将其判为非法。规范 0.3.2 解除限制后此类帧成为合法帧v1.5.2 的解压器修复随之跟进。仓库回归测试playTests.sh 中的对应用例touch tmp_empty zstd -d -o tmp2 $TESTDIR/golden-decompression/empty-block.zst $DIFF -s tmp2 tmp_empty测试逻辑非常直白先创建一个 0 字节的空文件tmp_empty再将黄金帧empty-block.zst解压最后用diff断言解压结果与空文件逐字节一致——即该帧必须成功解压出 0 字节内容。缺陷 4首个块为 RLE 块v1.4.3 修复CLI 专属最后受影响版本v1.4.3受影响组件仅 CLILibrary 不受影响参考压缩器是否产出否示例帧十六进制28b5 2ffd a001 0002 0002 0010 000b 0000 00缺陷现象zstdCLI解压器曾经拒绝这样的帧第一个块是 RLERun-Length Encoding块且其Block_Size为 131072即 128 KB同时帧内包含不止一个块。该示例帧的结构是两个 RLE 块第一个 RLE 块 131072 字节第二个 RLE 块 1 字节。值得注意的是这个缺陷只影响 zstd CLI不影响库——这是因为 CLI 的解压入口比库多了一层文件/流的处理逻辑在解析块头边界时对 RLE 块大小的处理存在偏差。这也是勘误表中唯一一个 Library 不受影响的条目。压缩器的规避策略由于历史 CLI 版本无法解码首块为满尺寸 RLE的帧参考压缩器选择在产出侧主动规避而不是只修解压侧。本仓库 zstd_compress.c 中有多处注释与代码体现了这一策略例如zstd_compress.c/* We dont want to emit our first block as a RLE even if it qualifies * because ... */zstd_compress.c 与 zstd_compress.c在常规压缩路径与多块压缩路径中都先判断ZSTD_isRLE()zstd_compress.c确认内容是否为 RLE若满足 RLE 条件却位于首块则改写为非 RLE 的常规压缩块。在内部模拟解压repcode 历史维护路径 zstd_compress.c 中同样保留注释dont emit the first block as RLE even if it qualifies。也就是说即使数据本身是完美的 RLE 候选例如整块全零字节压缩器也会刻意让第一个块以普通压缩块形式输出以换取与旧版 CLI 解压器的兼容性。代价是首块压缩率略微下降收益是跨版本兼容。回归测试playTests.sh 专门为该场景构造了 1 MiB 的全零输入进行对照验证# the following test verifies that the decoder is compatible with RLE as first block # older versions of zstd cli are not able to decode such corner case. dd bs1048576 count1 if/dev/zero oftmp zstd -d -o tmp1 $TESTDIR/golden-decompression/rle-first-block.zst $DIFF -s tmp1 tmp脚本注释直言旧版 zstd CLI 无法解码此类边角情况因此 zstd CLI 不产出它们以维持兼容性与文档描述完全吻合。缺陷 5微型 FSE 表 微型块v1.3.4 修复最后受影响版本v1.3.4受影响组件Library 与 CLI参考压缩器是否产出可能直到 v1.3.4 都曾产出但大概率从未实际发生示例帧十六进制28b5 2ffd 2027 c500 0080 f3f1 f0ec ebc6 c5c7 f09d 4300 0000 e0e0 0658 0100 603e 52缺陷现象这是勘误表中最古老、也最精巧的一条zstd 库曾经拒绝这样一类Compressed_Block——块内最后一个类型为FSE_Compressed_Mode的表其起始位置距离块末尾不足 4 字节。更形式化地描述设Last_Table_Offset为压缩块内不含块头最后一个FSE_Compressed_Mode表的起始偏移若满足Block_Content - Last_Table_Offset 4则旧版解压器会拒绝该块。这一条件成立的典型场景是最后一个序列化的 FSE 表占 2 字节且紧随其后的比特流bitstream只有 1 字节合计不足 4 字节。一个具体的触发构造文档给出了一个 5 字节Block_Content的构造示例块内仅有1 条序列Literals_Lengths_Mode字面量长度模式为FSE_Compressed_Mode且其序列化表大小为2 字节Offsets_Mode偏移模式为Predefined_Mode预定义表Match_Lengths_Mode匹配长度模式为Predefined_Mode比特流仅1 字节1 条序列恰好能用 1 字节表达。此时Block_Content总计 5 字节Last_Table_Offset为 25 - 2 3 4触发拒绝。这里的Predefined_Mode/FSE_Compressed_Mode等模式定义可对照 zstd_compression_format.mdCompression_Mode共有Predefined_Mode、RLE_Mode、FSE_Compressed_Mode、Repeat_Mode四种取值其中FSE_Compressed_Mode表示使用标准 FSE 压缩的分布表且规范要求仅当只有一个符号存在时不得使用。根因与修复这是 zstd 解压器早期对序列头/表解析边界判断过严的历史遗留它没有考虑到表 比特流的极短组合也可能合法。文档中给出的参考修复依据是 upstream 压缩器侧的 workaround 提交zstd_compress.c中约 L2667-L2682 处该行号对应 upstream 特定 commit本仓库版本行号可能略有差异。从本仓库 zstd_decompress_block.c 当前实现对nbSeq 0的短路处理可见解压器如今对序列段边界的判定已经足够宽容且精确。由于该缺陷自 v1.3.4 起修复距今已跨越多个大版本实际影响面集中在使用古董版本解压器的存量系统上。缺陷 6Magicless无魔数格式的误判v1.5.6 修复最后受影响版本v1.5.5受影响组件仅 LibraryCLI 不受影响参考压缩器是否产出是这是勘误表中唯一参考压缩器确实能产出的条目示例帧十六进制27 b5 2f fd 00 03 19 00 00 66 6f 6f 3f ba c4 59背景什么是 Magicless 格式zstd 的普通帧以 4 字节魔数0xFD2FB528小端序开头。而magicless 格式ZSTD_f_zstd1_magicless去掉了这 4 字节前缀帧直接从帧头字段开始。在本仓库解压器 zstd_decompress.c 中可以看到magicless 格式的帧头前缀长度被单独处理size_t const startingInputLength ZSTD_FRAMEHEADERSIZE_PREFIX(format); /* only supports formats ZSTD_f_zstd1 and ZSTD_f_zstd1_magicless */ assert( (format ZSTD_f_zstd1) || (format ZSTD_f_zstd1_magicless) );同时 zstd_decompress.c 中帧类型判定逻辑会显式跳过 magicless 帧的魔数检查并区分普通帧与 skippable可跳过帧。缺陷现象v1.5.6 修复了 magicless 格式解码器的一批缺陷导致其错误拒绝合法帧包括但不限于合法帧恰好以小端序的 legacy 魔数开头即 magicless 帧的第一个 4 字节内容恰好等于历史遗留格式legacy format的魔数被误判为 legacy 帧合法帧恰好以小端序的 skippable 魔数开头即 magicless 帧的第一个 4 字节恰好落在0x184D2A50~0x184D2A5F的 skippable 魔数区间zstd_decompress.c 的ZSTD_skippableFrame判断被误判为可跳过帧而走错解析路径。问题的本质是去掉魔数后magicless 帧的帧头内容是不可控的任何 4 字节组合都可能出现其中恰好命中 legacy 或 skippable 魔数的概率虽然低但并非为零一旦命中即被误判。受影响数据的恢复方案文档为无法立即升级到 v1.5.6 及以上的用户提供了明确的恢复方法将 zstd 魔数0xFD2FB528小端序前置拼接到受影响的数据前然后用标准格式解压器解压。# 以 shell 示意实际可用 printf 或任意二进制拼接工具 # 在受损数据前写入 4 字节小端序魔数 28 b5 2f fd再用标准解压器解压其原理正是以空间换兼容补回被 magicless 格式去掉的 4 字节魔数后帧重新变为标准格式标准解码路径不再需要猜测帧类型从而绕开误判。这也是勘误表中唯一提供了数据恢复方案的条目。勘误速查总表缺陷最后受影响版本受影响组件参考压缩器产出示例帧位置2 字节格式编码的 0 序列v1.5.5Library CLI否zeroSeq_2B.zst大小恰为 128 KB 的压缩块v1.5.2Library CLI否block-128k.zst0 字面量 0 序列压缩块v1.5.2Library CLI否28b5 2ffd 2000 1500 0000 00另有 empty-block.zst首块为满尺寸 RLE 块v1.4.3仅 CLI否28b5 2ffd a001 0002 0002 0010 000b 0000 00另有 rle-first-block.zst微型 FSE 表 微型块v1.3.4Library CLI可能曾产出十六进制帧见正文Magicless 格式误判v1.5.5仅 Library是27 b5 2f fd 00 03 19 00 00 66 6f 6f 3f ba c4 59给开发者与集成方的实践建议结合本仓库fluent-bit 内置 zstd 1.5.7的实际使用场景给出四条可落地的建议版本是第一道防线勘误表中最后受影响版本最高为 v1.5.5magicless 与 0 序列两条因此解压器版本≥ v1.5.6即可覆盖表中全部已修复缺陷。本仓库使用的 zstd 1.5.7 已满足要求可直接以 zstd_decompress_block.c 与 zstd_decompress.c 为参考实现。自研/移植解压器时逐条对照如果正在基于本仓库源码移植 zstd 解压逻辑请重点检查四类边界(a)nbSeq 0时的立即短路zstd_decompress_block.c(b) 压缩块大小恰好等于ZSTD_BLOCKSIZE_MAX128 KB时的放行(c) 0 字面量 0 序列空压缩块的放行(d) magicless 模式下帧头 4 字节命中 legacy/skippable 魔数时的误判规避。回归测试资产可直接复用仓库 tests/golden-decompression 目录下的 4 个黄金帧与 playTests.sh 中的测试逻辑可以原样移植进自己的 CI 管道配合 golden.sh 的正向批量测试与 detectErrors.sh 的反向损坏帧测试形成完整闭环。遇到合法帧解压失败时先查勘误表当对接的第三方系统报告解压失败而用zstd -t或库接口校验帧结构又完全正常时优先检查是否为上述边界帧若涉及 magicless 数据且无法升级可用前置拼接0xFD2FB528魔数的恢复方案先行抢救数据。总结zstd 的这份勘误文档是压缩领域少见的规范演进与实现缺陷双重视角档案它既揭示了帧格式规范在 0.3.2 版本对Compressed_Block限制的放宽缺陷 2、3 的根源也展示了实现细节中的两类典型问题——对编码格式取值边界判断过严缺陷 1、5、6与 CLI/库两条解析路径的行为不一致缺陷 4。通过本仓库的源码与黄金测试资产开发者可以完整复现每一条缺陷、理解修复逻辑并将其固化为自身的兼容性测试用例从而在自己的解压实现中规避同类问题。【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站