redis 8.8.1安全更新:一类精心构造的 RESTORE 载荷可触发越界写入,RDB 加载链路全面加固

2026年7月29日,redis 8.8.1 Latest 版本发布。本次更新最值得关注的内容并不是常规功能优化,而是一项明确标注为 SECURITY 的安全修复。

此次安全问题涉及 RedisBloom 与 TDigest 模块:攻击者如果构造恶意的 RESTORE 载荷,可能触发越界写入,进一步存在远程代码执行风险。对应修复围绕 RDB 加载与 RESTORE 恢复路径展开,对 Bloom、Cuckoo 以及 TDigest 三类数据结构的序列化元数据、缓冲区长度、容量和节点计数进行了系统性校验。

从更新内容可以看出,这次修复的核心目标非常明确:

  • 不再盲目信任 RDB 或 DUMP 载荷中携带的元数据
  • 在内存分配与数据复制前完成严格检查
  • 对截断、伪造、长度不一致的模块载荷直接拒绝加载
  • 保证合法快照的既有行为不发生变化
  • 通过针对性的损坏 RDB 回归测试,确认服务在收到异常 RESTORE 请求后仍能保持健康

需要特别注意的是,提交记录中还包含了补丁版本提升至 8.8.2 的变更,并且后续主仓库更新内容也指向 RedisBloom v8.8.2。也就是说,虽然发布信息标题显示为 8.8.1 Latest,但安全修复补丁链路中明确出现了 8.8.2 版本提升与集成更新。


一、漏洞概览:恶意 RESTORE 载荷可能导致越界写入

本次安全修复描述指出,RedisBloom 和 TDigest 中经过精心构造的 RESTORE payload,可能触发 out-of-bounds writes,也就是越界写入。

越界写入的危险性非常高。因为它意味着程序可能会在预期内存边界之外写入数据,造成内存布局被破坏。根据此次发布说明,攻击者可能利用这一类问题进一步导致远程代码执行。

问题聚焦于模块的 RDB 恢复逻辑。

在 Redis 中,RDB 是一种持久化快照格式。模块在保存和恢复自身数据时,会把内部状态序列化为 RDB 数据。当服务执行 RESTORE,或者在启动、加载快照时读取模块数据,就会进入对应模块的 RDB 加载流程。

如果加载流程直接相信序列化数据里记录的长度、容量、过滤器数量、桶数量或节点计数,而没有验证这些数据与实际缓冲区是否匹配,就可能出现多种危险情况:

  • 根据恶意容量值分配异常内存
  • 按错误长度复制数据
  • 缓冲区长度与位图大小不一致
  • 节点数量超过已分配数组的容量
  • 整数缩窄转换前缺少边界检查
  • 截断载荷被当作完整数据继续解析
  • 元数据和实际序列化 blob 大小不一致

这些问题都会造成缓冲区大小与实际访问范围脱节,也就是更新说明中提到的 buffer 或 size desync,最终可能引发越界访问。

本次修复的方向,是在进入分配、复制、反序列化等敏感操作之前,先判断输入是否可信。只要发现数据缺失、为空、过大、长度不匹配、计数异常、桶数量非法,模块就应拒绝加载该数据,而不是继续执行。


二、修复背景:这是一次针对 RDB restore 路径的高风险加固

此次变更被明确标注为 High Risk,也就是高风险修改。

原因并不难理解:RDB restore 路径属于安全敏感路径。它既要解析外部输入,又要恢复复杂的数据结构,还涉及内存分配、二进制数据拷贝、整数转换、数组索引和节点恢复。

一旦输入被伪造,原本用于描述数据结构的元信息就可能变成攻击入口。

更新说明中提到,过去的行为可能存在 buffer 或 size desync,也就是缓冲区和大小信息失去同步。比如序列化数据声称自己有某个容量、某个桶数量或某个节点数量,但实际提供的 blob 长度却不足;如果代码根据声明的长度继续读取或写入,就可能跨越有效内存范围。

此次加固承诺的兼容性原则也很清晰:

对于有效的快照,行为应保持不变。

也就是说,本次不是调整正常数据的存储格式或业务语义,而是为加载过程增加输入校验。正常生成、完整且未被篡改的 RDB 或 DUMP 数据,应继续按照原有方式恢复。变化主要发生在异常输入场景:伪造、截断、长度不一致、容量不合理的载荷将被拒绝。


三、Bloom 加载逻辑加固:不再信任 bitset 元数据

Bloom Filter 的 RDB 加载逻辑是本次重点修复对象之一。

在 Bloom 的加载流程中,修复内容主要包括以下方面:

  • 拒绝缺失的 bitset buffer
  • 拒绝空的 bitset buffer
  • 拒绝过大的 bitset buffer
  • 对较新的编码版本,让 bits 与 buffer size 建立明确对应关系
  • 对 encver 为 0 的旧编码版本,保留原有最低字节数检查
  • 对 n2 进行边界限制
  • 继续执行 bloom_validate_integrity 完整性校验

这些改动看似都是"检查",但实际上直接覆盖了越界风险最容易出现的关键节点。

1. 拒绝缺失、空和过大的 bitset buffer

Bloom Filter 的核心是位图,也就是 bitset。加载时需要从 RDB 中读取位图数据,再根据相关元数据构建过滤器状态。

如果 bitset buffer 缺失,意味着元数据存在但实际数据不存在;如果 buffer 为空,则可能无法满足位图读写需求;如果 buffer 过大,则可能导致异常的内存处理、整数计算或长度关系失真。

修复后,加载逻辑会在继续使用 bitset 前判断缓冲区是否符合预期。对于缺失、为空或尺寸异常的情况,将不再继续信任并加载。

2. 新编码版本中绑定 bits 与 buffer size

对于较新的编码格式,修复要求 bits 与 buffer size 之间必须满足正确关系。

这项校验十分关键。位数决定 Bloom Filter 可访问的 bit 范围,而 buffer size 决定实际可用的字节数。如果两者不一致,例如 bits 描述的可访问范围远大于实际 buffer 所能承载的范围,那么后续位操作就有可能触及缓冲区之外。

因此,新编码中不再只是读取 bits 和 buffer size,而是要求二者彼此匹配。

3. 保持 encver 为 0 的兼容处理

对于 encver 为 0 的旧编码版本,修复没有简单套用新格式的规则,而是保留 legacy minimum-byte check,也就是旧版本的最小字节数检查。

这体现出修复并非粗暴拒绝旧数据,而是在维持兼容性的前提下进行加固。旧编码可能具有不同的数据组织方式,因此保留其已有的最低字节校验逻辑,避免合法历史快照因格式差异被误判。

4. 对 n2 进行范围限制

Bloom 加载过程中还增加了对 n2 的边界约束。

n2 作为序列化元信息的一部分,如果没有被限制,可能参与后续大小计算、容量推导或索引逻辑。当恶意输入为其提供异常值时,就可能引发整数溢出、尺寸计算错误或数据结构状态异常。

因此,在进入更深层的恢复流程之前限制 n2,是防止攻击链继续向下传播的重要步骤。

5. 继续执行完整性验证

修复后,Bloom 仍然会调用 bloom_validate_integrity。

这意味着新增的长度和元数据检查,并不会替代已有完整性验证;两类校验共同存在。前者负责避免在危险数据上进行错误分配或复制,后者继续确认恢复后的 Bloom 数据结构本身是否满足内部一致性要求。


四、Cuckoo 加载逻辑加固:先校验,再缩窄,再分配

除了 Bloom Filter,Cuckoo Filter 的 RDB 加载路径也获得了强化。

本次针对 Cuckoo 的关键改动包括:

  • 在转换为 uint16 之前验证 filter count 和 bucket limit
  • 在为 sub-filter 分配内存之前调用 CuckooFilter_ValidateIntegrity
  • 将原有 debug assert 替换为显式检查
  • 校验每个 sub-filter 序列化 blob 的长度
  • 要求 blob 长度必须等于 bucketSize 乘以 numBuckets
  • 要求 numBuckets 必须为 2 的幂

这些变更可以概括为一句话:所有依赖外部序列化元数据的内存操作,都要先经过可信度验证。

1. 防止整数缩窄带来的问题

修复中强调,在 narrowing to uint16 之前验证过滤器数量和桶数量限制。

所谓缩窄转换,指的是一个较大范围的数值被转换成更小范围的整数类型。例如外部数据中可能存放一个较大的数,而代码后续将其转换为 uint16。

如果在转换前不检查范围,那么超过 uint16 可表示范围的数据可能发生截断或回绕。最终程序看到的数值与攻击者提供的原始数值不一致,后续分配、循环、索引和长度计算也可能建立在错误值之上。

因此,修复要求先验证 filter count 和 bucket limit 是否处于安全范围,再转换为 uint16。

2. 分配 sub-filter 之前先进行完整性校验

修复中提到,在分配 sub-filter 前调用 CuckooFilter_ValidateIntegrity。

这一顺序非常重要。

如果先根据未经验证的元数据分配 sub-filter,再去检查数据是否有效,那么风险已经提前发生。异常数量、异常桶规模或不合理参数可能导致错误分配、资源异常消耗,甚至触发后续访问问题。

现在的逻辑是先验证整体结构与关键参数,再进入 sub-filter 的内存分配过程。

3. 用显式检查替换调试断言

原先部分场景使用 debug assert。此次修复将其替换为显式检查。

调试断言通常用于开发阶段发现不符合预期的状态,但在实际生产构建中,断言未必能提供足够的运行时防护。对于来自 RDB 或 RESTORE 的不可信输入,不能仅仅依赖开发期假设,而必须在运行时明确检查,并在非法条件出现时返回错误。

因此,显式检查能够让异常输入得到可控拒绝,而不是依赖断言行为。

4. 强制验证序列化 blob 长度

Cuckoo 的每个 sub-filter 都需要验证其序列化 blob 长度。

修复要求:

每个 sub-filter 的序列化 blob 长度必须等于 bucketSize 乘以 numBuckets。

这条规则直接把逻辑结构和实际二进制数据长度绑定起来。

如果 bucketSize 与 numBuckets 描述了一个过滤器应有的存储规模,而传入 blob 的实际长度与该规模不相等,就说明载荷已经不可信:可能被截断、被伪造,或者元数据被修改。

在这种情况下继续加载,后续读取、复制或访问都可能超出实际 blob 的边界。

5. numBuckets 必须是 2 的幂

修复还要求 numBuckets 必须是 power-of-two,也就是 2 的幂。

Cuckoo Filter 的桶数量通常与其索引和定位逻辑密切相关。如果桶数不符合预期约束,可能导致定位算法、掩码计算或桶访问逻辑出现异常。

因此,恢复阶段也必须重新确认该约束,而不是假设序列化数据天然合法。


五、TDigest 加载逻辑加固:容量与节点计数必须匹配

TDigest 是本次安全修复涉及的另一重点模块。

针对 TDigest 的 RDB 加载,修复内容包括:

  • 确保 td_new 调用成功
  • 要求加载得到的 cap 必须与根据 compression 分配的数组容量一致
  • 加强 merged node count 与 unmerged node count 相对于 cap 的检查
  • 修复节点数量溢出检查问题
  • 通过篡改 cap 的 DUMP 数据验证 RESTORE 被拒绝

TDigest 内部需要加载容量、已合并节点数、未合并节点数以及相关节点数据。由于这些值直接决定数组大小、节点存储和加载循环范围,所以任何不一致都可能造成严重问题。

1. 确保 td_new 成功

加载流程首先要确保 td_new 成功。

如果对象创建失败,却继续访问其内部数组、容量字段或节点结构,就会带来不可预期的行为。因此,修复明确要求在后续加载前确认 td_new 返回成功。

2. loaded cap 必须匹配 compression 分配出的数组容量

此次修复要求,从序列化数据中读取的 cap,必须与根据 compression 创建并分配出的数组容量保持一致。

这一点是 TDigest 加固的核心。

如果载荷中的 cap 被攻击者篡改为一个异常值,而代码仍根据它读取节点、写入数组或确定边界,就可能造成数组访问越界。修复后,cap 不再只是一个可以直接使用的外部数值,而是必须与实际已分配数组容量对得上。

换句话说,序列化数据中的容量声明不能凌驾于程序真实分配出来的内存容量之上。

3. 加强 merged 与 unmerged 节点数量校验

修复还收紧了 merged nodes 和 unmerged nodes 相对于 cap 的检查。

TDigest 会保存已合并节点和未合并节点。如果节点计数超过容量,或者两类节点计数关系异常,后续加载节点时就可能写入数组边界之外。

因此,加载时必须确认:

  • merged node count 没有超过允许范围
  • unmerged node count 没有超过允许范围
  • 两类节点数量与 cap 之间保持合法关系
  • 不会因为计数相加或比较而发生溢出问题

提交记录中还明确包含了一项"修复 TDigest RDB load node count overflow check"的修改,说明节点数量的溢出检查是本次修复中的一个具体补充点。


六、版本与提交信息:从安全加固到补丁版本提升

此次修复是一次回移植工作,说明中明确表示这是将另一条修复内容 backport 到 8.8 分支。

变更摘要中包括以下提交内容:

  • Validate Bloom RDB bitset size on load,对 Bloom RDB bitset 尺寸进行加载时验证
  • RDB hardening,对 RDB 加载路径进行加固
  • Bump version to 8.8.2,将版本提升至 8.8.2
  • Fix TDigest RDB load node count overflow check,修复 TDigest RDB 加载节点计数溢出检查

合并后,相关变更进入 8.8 分支,提交记录显示最终合并提交为 77dea3b。

此外,后续在主 Redis 仓库中还出现了"Update RedisBloom to v8.8.2"的集成更新,相关提交为 91821fe。

这意味着,从修复提交的版本演进来看,安全加固最终与 RedisBloom 8.8.2 的更新关联在一起。


七、测试覆盖:专门构造损坏 DUMP,验证 RESTORE 失败但服务健康

安全修复不能只看代码检查是否增加,还必须验证异常输入是否真的被拒绝,同时确认服务器不会因为恶意数据崩溃或进入异常状态。

此次更新给出了针对性测试信息:

  • 执行 gmake build
  • 定向 corrupt-RDB 测试套件 3 项全部通过
  • 新增 flow tests
  • 对 BF 与 CF 使用缩短模块字符串的损坏 DUMP
  • 对 TDigest 使用膨胀 cap 的损坏 DUMP
  • 验证 RESTORE 返回错误
  • 验证服务器保持健康

这套测试思路与漏洞根源高度对应。

对于 Bloom 和 Cuckoo,测试通过缩短 module string 的方式构造不完整数据。这样可以验证:当元数据与实际模块数据不匹配,或者序列化内容被截断时,加载流程是否会错误继续执行。

对于 TDigest,测试通过将 cap 篡改为膨胀值,也就是人为增大容量字段,验证加载逻辑是否会信任伪造的 cap,还是能发现该值与实际根据 compression 分配出的数组容量不一致。

测试预期并不是"恢复成功",而是 RESTORE 应返回错误。同时,服务应仍处于健康状态。

这两个结果缺一不可:

  • 返回错误,说明恶意或损坏载荷已被正确拒绝
  • 服务器健康,说明拒绝过程没有导致进程崩溃、内存破坏或后续服务异常

八、自动化审查发现的测试解析问题:缺少 SINT opcode 支持

在自动化代码审查过程中,还发现了测试文件中的一个问题。

问题位于用于篡改 TDigest DUMP 的解析辅助逻辑。该解析器只处理了以下模块 opcode:

  • UINT,值为 2
  • DOUBLE,值为 4
  • STRING,值为 5
  • EOF,值为 0

但它没有处理 SINT,值为 1。

审查指出,TDigest 的 RDB 格式会通过 RedisModule_SaveSigned 保存多个关键字段,包括:

  • cap
  • merged_nodes
  • unmerged_nodes
  • total_compressions

由于这些字段以 SINT opcode 写入,解析函数在读取完 3 个 double 后,会立刻遇到 opcode 1。如果解析器不支持该类型,就会抛出"Unknown module opcode"的运行时错误。

这意味着,用于构造 TDigest 损坏数据的测试本身会失败,而不是正常完成载荷篡改并验证 RESTORE 拒绝行为。

自动化审查将这一问题标记为 High Severity,并指出该问题会让 TDigest corruption test 始终因为异常而失败。

这条审查反馈的意义在于:安全修复不仅要覆盖生产代码的 RDB 解析逻辑,测试代码同样必须能够准确解析模块编码格式。否则测试看似存在,实际上无法真正验证目标场景。


九、质量检查结果:通过质量门禁,但新增代码覆盖率为零

相关变更还经过了质量平台检查,结果显示 Quality Gate Passed,也就是质量门禁通过。

检查结果包括:

  • 1 个新增问题
  • 0 个已接受问题
  • 0 个 Security Hotspots
  • 新增代码覆盖率为 0.0%
  • 新增代码重复率为 0.0%

其中,新增问题与自动化审查发现的 TDigest 测试解析器缺少 SINT opcode 支持相呼应。

值得注意的是,质量门禁虽然通过,但新增代码覆盖率显示为 0.0%。与此同时,变更说明又明确表示定向 corrupt-RDB 测试套件 3 项全部通过。

这反映出质量平台统计的覆盖率指标与实际执行的定向测试并不一定完全等价:安全修复已经有针对性测试验证,但在质量平台的新增代码覆盖率统计中仍显示为零。


十、此次安全修复覆盖的完整逻辑链路

综合本次更新内容,可以将加固链路概括为以下过程。

第一步:识别外部序列化元数据不可直接信任

RDB 和 DUMP 载荷中的容量、长度、桶数、过滤器数量、位数、节点计数等字段,都可能被恶意修改。因此它们不能被直接用于分配内存、复制数据或计算访问范围。

第二步:在分配之前验证元数据

Bloom 检查 bitset buffer 与 bits 的对应关系。

Cuckoo 检查 filter count、bucket limit、bucket 数量及 blob 长度。

TDigest 检查 cap 与实际分配容量是否一致,并检查节点计数是否超出 cap。

第三步:在复制之前验证实际数据长度

对于 Cuckoo,序列化 blob 的长度必须严格等于 bucketSize 乘以 numBuckets。

对于 Bloom,bitset buffer 不能缺失、为空或过大。

对于 TDigest,cap 不允许与真实数组容量脱节。

第四步:保留或继续执行内部完整性检查

Bloom 继续执行 bloom_validate_integrity。

Cuckoo 在分配 sub-filter 前调用 CuckooFilter_ValidateIntegrity。

这些完整性检查与新增边界检查共同构成防线。

第五步:异常输入必须以错误形式被拒绝

缩短的 BF、CF 模块字符串,以及篡改后的 TDigest cap,都应使 RESTORE 失败。

第六步:服务必须保持健康

拒绝异常载荷不能以崩溃、内存破坏或服务不可用为代价。测试明确验证服务器保持健康。


十一、结语:RDB 恢复不是普通读取,而是安全边界

代码地址:github.com/redis/redis

redis 8.8.1 这次更新传递了一个非常重要的信号:RDB 加载与 RESTORE 恢复路径,本质上是处理外部二进制输入的安全边界。

对于 Bloom、Cuckoo 和 TDigest 这类拥有复杂内部结构的数据类型,仅靠"正常快照一定正确"的假设并不安全。位图长度、桶数量、过滤器数量、节点容量、序列化 blob 大小和节点计数,只要有一个字段被伪造并被直接信任,就可能造成内存边界失控。

本次修复通过以下措施降低风险:

  • Bloom 校验 bitset buffer、bits 与 n2
  • 兼容 encver 为 0 的旧格式最小字节检查
  • Cuckoo 在缩窄整数前校验范围
  • Cuckoo 在分配前验证完整性
  • Cuckoo 严格验证 bucketSize、numBuckets 与 blob 长度关系
  • TDigest 验证 td_new 创建结果
  • TDigest 将 loaded cap 与 compression 对应的实际数组容量绑定
  • TDigest 加强 merged 与 unmerged 节点数量边界检查
  • 增加损坏 DUMP 的 RESTORE 回归测试
  • 确认非法载荷被拒绝后服务依旧健康

从发布信息到后续提交记录,这次安全加固最终伴随 RedisBloom 8.8.2 的版本提升和主 Redis 仓库集成更新。对于使用 RedisBloom、Cuckoo Filter、Bloom Filter 或 TDigest 的环境而言,这类针对 RDB restore 路径的修复具有直接的安全价值。

相关推荐
無a伟2 小时前
Redis持久化详解:RDB与AOF原理、配置、优缺点
数据库
muddjsv3 小时前
SQLite 外键进阶:CASCADE / SET NULL / 级联更新与完整性校验
数据库·sqlite
APItesterCris3 小时前
Open Claw 实战教程:5 分钟搭建京东商品自动化监控与数据分析系统
大数据·运维·数据库·数据仓库·自动化
Nturmoils4 小时前
查库存的 SQL 时灵时不灵,最后发现是 WHERE 里两个函数在打架
数据库
不想纳尼的青春4 小时前
where = 的作用?会影响性能吗?count(*) 和 count()哪个快?
数据库·oracle
倔强的石头_5 小时前
Spring Boot 接入金仓数据库:配置分层、启动自检与常见错误
数据库
黑桃小柒75 小时前
Django模型关系:从一对多到多对多全解析
数据库·django·sqlite
上海安当技术5 小时前
统一身份认证平台怎么落地?11 个异构业务系统接入 ASP 的完整实施路径
数据库·servlet·架构·kubernetes·jenkins
晚安code5 小时前
RocketMQ 顺序消费实战:批量接口加 Redis Pipeline同步数据
redis·rocketmq
西门啐血6 小时前
Vue 缓存之坑,变量赋值方式和响应式数据
前端·vue.js·缓存