:zstd 曾经会拒绝哪些“合法”压缩帧,以及压缩器如何主动规避)
Zstandard 解码器已知缺陷备忘Erratazstd 曾经会拒绝哪些“合法”压缩帧以及压缩器如何主动规避【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文以 MongoDB 仓库内打包的 Zstandard 第三方实现中的 decompressor_errata.md 为主体完整还原三份“解码器误拒合法帧”的历史缺陷记录每个缺陷最后影响的版本、受影响的组件库/CLI、参考压缩器是否可能产生该帧、示例帧字节、以及 bug 的精确触发条件并结合当前仓库中 zstd_compress.c 的源码说明压缩器侧为兼容旧版本解码器而内置的两处主动规避workaround逻辑。读完本文你可以准确判断“某个版本 zstd 解码器会拒绝哪些合法输入”并理解 zstd 压缩器代码中若干看似多余的防御性分支背后的兼容性动机。一、文档定位什么是“Decompressor Errata”Errata 是 Zstandard 格式维护方维护的一份已知解码器缺陷清单专门记录“解码器错误地拒绝了合法 zstd 帧”的 bug而不是解码出错、崩溃这类 bug。文档规定每条记录必须包含五个要素最后受影响的解码器版本Last affected version受影响的解码器组件Library、CLI 或两者参考压缩器reference compressor是否可能生成过这种帧一个可复现的最小示例帧十六进制字节缺陷本身的精确描述。条目按逆时间序排列影响最新版本号的 bug 排在最前面。这份文档对下游用户有三重价值排查“我的合法文件为什么解码报错”、评估跨版本部署的兼容性风险以及理解压缩器源码中兼容性规避代码的由来。二、缺陷 10 字面量、0 序列的 Compressed_Block最后影响 v1.5.2最后受影响版本v1.5.2受影响组件Library 与 CLI 均受影响参考压缩器是否产生过此类帧否No示例帧28b5 2ffd 2000 1500 0000 00缺陷描述zstd 解码器错误地拒绝了这样一种Compressed_Block——它把字面量以Raw_Literals_Block模式编码但字面量为零个同时也不含任何序列sequence。为什么这种帧是“合法”的从 zstd_compression_format.md 所对应的规范历史看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).规范版本 0.3.2 之前这个“零字面量 零序列”的压缩块被明确禁止规范 0.3.2 之后该限制被移除上游 facebook/zstd 的 PR#1689即此后这类帧在规范层面合法但 v1.5.2 及以前的解码器仍会拒绝它们。由于参考压缩器从未生成过这种块实际生态中遇到它的概率极低——它的意义主要在于规范与实现的边界对齐以规范为准该帧可解码。三、缺陷 2首块为 RLE 块最后影响 v1.4.3仅 CLI最后受影响版本v1.4.3受影响组件仅 CLI库不受影响参考压缩器是否产生过此类帧否但压缩器曾通过规避来保证这一点示例帧28b5 2ffd a001 0002 0002 0010 000b 0000 00缺陷描述zstd命令行工具的解码路径会在如下情形拒绝帧——第一块是 RLE 块、其Block_Size恰为 131072128 KiB且该帧包含多于一个块。上面的示例帧就是一个 131072 字节的 RLE 块后接一个 1 字节的 RLE 块。库library不受影响问题只存在于 CLI 解码路径。压缩器的规避代码首块绝不发 RLE这条缺陷催生了压缩器源码中最直观的一处兼容性 workaround。当前仓库 zstd_compress.c 中对应 errata 文档引用的历史提交 L3527–L3535if (!zc-isFirstBlock cSeqsSize rleMaxLength ZSTD_isRLE((BYTE const*)src, srcSize)) { /* We dont want to emit our first block as a RLE even if it qualifies because * doing so will cause the decoder (cli only) to throw a should consume all input error. * This is only an issue for zstd v1.4.3 */ cSeqsSize 1; }逻辑拆解ZSTD_isRLE()zstd_compress.c判断整块源数据是否为单一字节重复只有当!zc-isFirstBlock时块才会被允许走 RLE 输出后续cSeqsSize 1分支调用ZSTD_rleCompressBlock输出因此首块即使整块同字节也会按普通压缩块或 nocompress 块输出从而保证旧 CLI 解码器不会踩中该 bug。同一规避在文件中的块分裂partitioning路径里也重复出现L4258、L4291、L6744 附近均有 “We dont want to emit our first block as a RLE even if it qualifies” 的同款注释与判断说明该约束贯穿了单块与多块两种压缩调度路径。四、缺陷 3Tiny FSE Table Block最后影响 v1.3.4库 CLI最后受影响版本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缺陷描述解码器拒绝某类Compressed_Block——其最后一个FSE_Compressed_Mode类型熵表的起始位置距离块内容结尾不足 4 字节时即被误判为损坏。用文档中的记法设Last_Table_Offset为压缩块不含 3 字节头内最后一个FSE_Compressed_Mode表的起始偏移若Block_Content - Last_Table_Offset 4buggy 解码器就会拒绝该块。触发这一条件的典型组合文档给出的具体例子要素取值块内序列数1Literals_Lengths_ModeFSE_Compressed_Mode序列化表大小 2 字节Offsets_ModePredefined_ModeMatch_Lengths_ModePredefined_Mode序列比特流大小1 字节单条序列可装入 1 字节合计Block_Content 5字节Last_Table_Offset 25 - 2 3 4恰好落入拒绝区间。根因在于旧版本解码器中FSE_readNCount()读取表头时未正确校验“剩余缓冲 4 字节”的情形。压缩器的规避代码宁可发 nocompress 块当前仓库 zstd_compress.c 中保留了完整的规避分支对应 errata 引用的历史提交 L2667–L2682/* zstd versions 1.3.4 mistakenly report corruption when * FSE_readNCount() receives a buffer 4 bytes. * Fixed by https://github.com/facebook/zstd/pull/1146. * This can happen when the last set_compressed table present is 2 * bytes and the bitstream is only one byte. * In this exceedingly rare case, we will simply emit an uncompressed * block, since it isnt worth optimizing. */ if (lastCountSize (lastCountSize bitstreamSize) 4) { /* lastCountSize 2 bitstreamSize 0 lastCountSize 3 */ assert(lastCountSize bitstreamSize 3); DEBUGLOG(5, Avoiding bug in zstd decoder in versions 1.3.4 by emitting an uncompressed block.); return 0; /* 返回 0上层将按 nocompress未压缩块输出 */ }这里lastCountSize是最后一个序列化 FSE 表的大小bitstreamSize是ZSTD_encodeSequences()产出的序列比特流长度。两者之和小于 4 字节实际上断言了必等于 3因为lastCountSize 2且bitstreamSize 0时函数返回 0 让调用方退化为输出一个未压缩块。注释直言“exceedingly rare”“isnt worth optimizing”——压缩率损失可以接受换取与 ≤ v1.3.4 解码器的完全兼容。同样的 4 字节边界防御也出现在超级块路径 zstd_compress_superblock.c 中FSE_readNCount() receives a buffer 4 bytes的同款注释。五、三条缺陷的横向对比与工程启示缺陷最后影响版本组件参考压缩器是否产生过压缩器现存规避0 字面量 0 序列的 Compressed_Blockv1.5.2Library CLI否压缩器本就不产生该形态首块 RLE 且 Block_Size131072 多块帧v1.4.3仅 CLI否靠规避保证首块强制不走 RLE 输出末表距块尾 4 字节的 Compressed_Blockv1.3.4Library CLI可能≤ v1.3.4末表比特流 4 字节时退化为 nocompress 块三点启示均以上述文档与源码为依据误拒 bug 不等于解码器损坏数据三条记录的共同特征是“拒绝合法帧”fail-safe 方向出错不会解出错误数据但对可用性是硬伤因此上游选择在 errata 中登记而非简单当作实现瑕疵压缩器为最低版本解码器兜底zstd 的流式/库 API 长期面对异构的旧解码器其源码中isFirstBlock、lastCountSize bitstreamSize 4这类条件注释里都明确标注了所规避的具体版本≤ v1.4.3、≤ v1.3.4是把 errata 转化为常驻代码约束的典型案例版本判定应以本仓库实际代码为准当前 MongoDB 仓库内这份 Zstandard 的库版本为 zstd.h 中声明的 v1.5.5ZSTD_VERSION_MAJOR1, MINOR5, RELEASE5高于三条缺陷中最高影响版本 v1.5.2因此解码侧风险主要存在于外部旧环境而本仓库代码中的压缩侧规避分支仍保留用于产出能被更老解码器消费的数据。六、如何验证这些结论只读仓库内的路径缺陷记录全文src/third_party/zstandard/zstd/doc/decompressor_errata.md首块 RLE 规避src/third_party/zstandard/zstd/lib/compress/zstd_compress.c#L3974-L3982Tiny FSE Table 规避src/third_party/zstandard/zstd/lib/compress/zstd_compress.c#L2929-L2943RLE 判定函数src/third_party/zstandard/zstd/lib/compress/zstd_compress.c#L3412格式规范文档Compressed_Block/Raw_Block/RLE 块定义src/third_party/zstandard/zstd/doc/zstd_compression_format.md注意errata 原文给出的示例帧如28b5 2ffd 2000 1500 0000 00可用于人工构造解码测试由于本仓库为只读环境复现验证建议在你自己的环境中用上述字节序列配合对应历史版本的 zstd CLI/库执行以确认“旧版本拒绝、新版本接受”的行为差异。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考