深入解析 PaddlePaddle paddle.trace 堆缓冲区溢出漏洞(PDSA-2023-003 / CVE-2023-38671):从 PoC 触发路径到 InferMeta 边界校验修复
【免费下载链接】PaddlePArallel Distributed Deep LEarning: Machine Learning Framework from Industrial Practice (『飞桨』核心框架,深度学习&机器学习高性能单机、分布式训练和跨平台部署)项目地址: https://gitcode.com/GitHub_Trending/pa/Paddle
导读
本文围绕 PaddlePaddle 官方安全通告 PDSA-2023-003 展开,完整剖析paddle.trace算子中一个已被修复的堆缓冲区溢出(Heap Buffer Overflow)漏洞(CVE-2023-38671):包括官方 PoC 的触发条件、trace从 Python API 到 C++ Kernel 的参数流转路径、修复方案落点在 TraceInferMeta 中的边界校验逻辑,以及 PaddlePaddle 的安全模型与漏洞上报流程。读完后,你可以理解"越界 axis 参数为何能导致内存破坏"、如何在当前代码库中验证该漏洞的修复状态,以及如何在生产环境中安全使用此类张量 API。
1. 漏洞概述
| 项目 | 内容 |
|---|---|
| 通告编号 | PDSA-2023-003 |
| CVE 编号 | CVE-2023-38671 |
| 漏洞类型 | 堆缓冲区溢出(Heap buffer overflow) |
| 受影响 API | paddle.trace |
| 修复版本 | PaddlePaddle 2.5.0(官方通告声明该修复将包含在 2.5.0 中,修复提交为12549dfe3e87a4c30f852d2eca81d7f67c8daa87) |
| 报告者 | Tong Liu(ShanghaiTech University,上海科技大学) |
该漏洞的官方定义非常明确:paddle.trace存在堆缓冲区溢出,攻击者通过构造非法的axis1/axis2参数值即可触发。按照 PaddlePaddle 在 SECURITY.md 中对"漏洞"的判定标准——"如果恶意输入能触发内存破坏(memory corruption)或非干净退出(non-clean exit),这类缺陷即被视为安全问题"——paddle.trace在越界参数下导致的堆溢出显然满足这一标准,属于需要立即修复的高优先级代码安全问题。
值得强调的是,这与"参数不合法时抛出异常拒绝执行"是两回事。官方安全指南中明确指出:对于"意外的参数和行为,PaddlePaddle 会通过抛出 Python 异常或 C++ 返回错误状态进行检查,此时虽然仍可能导致拒绝服务,但退出是干净的,因此不算安全漏洞"。paddle.trace的严重性恰恰在于:非法参数没有被拦截,而是继续进入了底层内存计算路径,最终造成堆内存破坏。
2. 官方 PoC:触发条件逐行拆解
官方通告给出的最小复现脚本如下(完整继承自 pdsa-2023-003.md):
import paddle import numpy as np from paddle import trace x = paddle.to_tensor(np.random.uniform(-10, 10, [2, 2, 2]).astype(np.float64)) offset = paddle.to_tensor(np.random.uniform(-10, 10, []).astype(np.int32)) axis1 = paddle.to_tensor(np.random.uniform(-6666666, -2, []).astype(np.int32)) axis2 = paddle.to_tensor(np.random.uniform(-6666666, -2, []).astype(np.int32)) trace(x, offset, axis1, axis2)对照 paddle.trace 的 Python 接口定义,可以拆解出三个关键的"异常输入":
def trace( x: Tensor, offset: int = 0, axis1: int = 0, axis2: int = 1, name: str | None = None, ) -> Tensor:- 参数类型异常:
offset、axis1、axis2的声明类型都是int,但 PoC 传入的是 0 维paddle.Tensor(int32)。动态图模式下框架会将标量张量转换为 Python 标量继续执行,因此类型异常本身不是根因,但反映了该 API 入口对输入约束的依赖。 - 数值范围异常(真正的触发点):
axis1、axis2取值范围是[-6666666, -2]内的随机 int32,即远超合法范围的巨大负数。对一个 3 维输入张量而言,合法的 axis 取值区间是[-3, 2](即[-len(shape), len(shape)-1])。-6666666这样的值在归一化(dim + ndim)后会得到一个绝对值巨大的非法维度索引。 - 输入张量本身完全合法:
x是[2, 2, 2]的 float64 张量,数据值也在正常范围内。这说明不需要构造恶意数据,仅靠越界 axis 参数就能触发内存破坏,攻击面非常小、触发成本极低。
一个容易混淆的语义边界需要特别说明:paddle.trace的文档明确写道——"如果 offset 超出 axis1、axis2 所指示的输入 shape 范围,将返回 0"(见 math.py 文档字符串)。也就是说,越界的offset是合法语义(返回零值),而越界的axis1/axis2是非法输入(应被拒绝)。漏洞的本质正是后者未被拦截,非法值被传递到了底层维度计算中。
3. 参数流转路径:从 Python 调用到 C++ 内存操作
理解漏洞机理,需要看清trace的参数在框架中经历的完整链路。当前代码库中该链路分为两条分支(见 math.py 实现):
if in_dynamic_or_pir_mode(): return _C_ops.trace(x, offset, axis1, axis2) else: __check_input(x, offset, axis1, axis2) # ... LayerHelper append_op(type='trace', attrs={'offset':..., 'axis1':..., 'axis2':...})- 动态图 / PIR 模式:直接透传给
_C_ops.trace,Python 层不做任何数值范围检查,校验责任完全下沉到 C++ 侧(即 InferMeta 层)。 - 静态图模式:先在 Python 层执行
__check_input,对 axis 做assert范围校验,再以attrs形式写入算子描述符。
而__check_input的校验逻辑(math.py)对 axis 的处理方式是:
axis1_ = axis1 if axis1 >= 0 else len(input_shape) + axis1 # ... assert (0 <= axis1_) and (axis1_ < len(input_shape)), ( f"The argument axis1 is out of range (expected to be in range of " f"[{-(len(input_shape))}, {len(input_shape) - 1}], but got {axis1})." )即:负数 axis 先做ndim + axis归一化,再要求归一化结果落在[0, ndim)内,且要求axis1 != axis2。对于 PoC 中的-6666666,归一化后仍为巨大的负数,该校验会失败。
关键在于:PoC 走的是动态图分支(脚本未创建Program上下文,默认动态图),Python 侧的__check_input根本不会执行,参数原封不动进入 C++ 层。在存在漏洞的版本中,C++ 侧对 axis 缺少等价的边界校验,非法值继续参与输出维度的推导与对角线计算,最终导致越界的堆内存读写——这就是"堆缓冲区溢出"的成因链。
4. 修复落点:TraceInferMeta 中的边界校验
当前代码库中,trace的输出形状推导函数 TraceInferMeta 对输入做了完整的防御性校验,与通告所述"已修复"的状态一致:
void TraceInferMeta( const MetaTensor& x, int offset, int axis1, int axis2, MetaTensor* out) { int dim1 = axis1; int dim2 = axis2; auto x_dims = x.dims(); int dim1_ = dim1 < 0 ? x_dims.size() + dim1 : dim1; int dim2_ = dim2 < 0 ? x_dims.size() + dim2 : dim2; PADDLE_ENFORCE_GE(x_dims.size(), 2, common::errors::OutOfRange( "Input(x)'s dim is out of range (expected at least 2, but got %ld).", x_dims.size())); PADDLE_ENFORCE_LT(dim1_, x_dims.size(), common::errors::OutOfRange( "axis1 is out of range (expected to be in range of [%ld, %ld], but got %ld).", ...)); PADDLE_ENFORCE_GE(dim1_, 0, common::errors::OutOfRange( "axis1 is out of range (expected to be in range of [%ld, %ld], but got %ld).", ...)); PADDLE_ENFORCE_LT(dim2_, x_dims.size(), common::errors::OutOfRange(...)); PADDLE_ENFORCE_GE(dim2_, 0, common::errors::OutOfRange(...)); PADDLE_ENFORCE_NE(dim1_, dim2_, common::errors::InvalidArgument( "The dimensions should not be identical %ld vs %ld.", dim1, dim2)); // ... 随后才基于 dim1_/dim2_ 推导输出 shape }逐条校验覆盖了三类非法输入:
| 校验项 | 拒绝的输入 | 错误类型 |
|---|---|---|
x_dims.size() >= 2 | 少于 2 维的输入张量 | OutOfRange |
0 <= dim1_ < ndim且0 <= dim2_ < ndim | 越界的 axis1 / axis2(正是 PoC 的-6666666) | OutOfRange |
dim1_ != dim2_ | axis1 与 axis2 指向同一轴 | InvalidArgument |
这意味着,把 PoC 中的axis1 = -6666666放入当前代码库,会在 InferMeta 阶段被PADDLE_ENFORCE_GE(dim1_, 0, ...)干净地拦截并抛出OutOfRange异常——即从"内存破坏"降级为"预期内的错误处理",符合官方安全模型对非漏洞行为(clean exit)的定义。由于 InferMeta 在 Kernel 执行之前被调用,非法值不再有机会流入后续的内存分配与计算路径,这正是修复能够阻断堆溢出的根本原因。
5. 修复后的内核实现:TraceKernel 与 Diagonal
InferMeta 通过之后,实际的计算由 CPU TraceKernel 完成(GPU 侧有对应的 trace_kernel.cu):
template <typename T, typename Context> void TraceKernel(const Context& dev_ctx, const DenseTensor& x, int offset, int axis1, int axis2, DenseTensor* out) { auto* out_data = dev_ctx.template Alloc<T>(out); if (out && out->numel() == 0) { return; } const DenseTensor diag = funcs::Diagonal<T, Context>(dev_ctx, &x, offset, axis1, axis2); if (diag.numel() > 0) { auto x = EigenMatrix<T>::Reshape(diag, diag.dims().size() - 1); auto output = EigenVector<T>::Flatten(*out); auto reduce_dim = Eigen::array<int, 1>({1}); output.device(*dev_ctx.eigen_device()) = x.sum(reduce_dim); out->Resize(out->dims()); } else { std::fill(out_data, out_data + out->numel(), static_cast<T>(0)); } }可以看到内核本身并不做 axis 合法性判断,它假定Diagonal拿到的 axis 一定合法,直接基于其构造对角线张量并做 Eigen 归约求和;只有对角线元素数为 0 时(对应"offset 越界返回 0"的合法语义)才用std::fill写零填充。这一"内核不做防御、校验前置到 InferMeta"的分层设计,也从侧面印证了:一旦 InferMeta 的边界校验缺位,Diagonal中的维度/偏移计算就是漏洞利用的直接载体。内核通过PD_REGISTER_KERNEL注册了 float、double、int、int64_t、float16、complex64、complex128 等多种 dtype 的 CPU 版本,说明该 API 的暴露面较广,校验缺失的影响也随之放大。
6. PaddlePaddle 的安全模型与漏洞上报
PDSA-2023-003 通告末尾引导读者参阅 SECURITY.md 了解安全模型。结合该文档,可以把本漏洞放在 PaddlePaddle 的整体安全体系里理解:
漏洞判定标准。官方将"模型执行任意计算(读写文件、网络通信)导致的内存耗尽、死锁"等视为框架的固有能力而非漏洞,除非"超出相关操作的本意";同时明确只有"恶意输入触发内存破坏或非干净退出"才定性为安全漏洞。paddle.trace堆溢出正落在后者范畴内。
漏洞上报流程。安全团队鼓励负责任披露(responsible disclosure),要求报告者提交:漏洞详情与可复现的 PoC(本通告中完整保留了报告者的 PoC 即为范例)、攻击场景与攻击者可达成的目标、漏洞是否已公开、报告者姓名与所属机构。修复发布后,官方会在安全通告中公开漏洞细节与报告者署名(本例为上海科技大学的 Tong Liu),也可以选择匿名。
自动化漏洞发现工具。官方提到已开源部分代码安全工具(含动态的算子 fuzzer 样本和静态的 CodeQL 查询样本),供安全研究者编写自己的 fuzzer 或 QL 查询,测试更多 PaddlePaddle 模块。像paddle.trace这类"Python 入口宽松 + C++ 深层计算"的算子,正是参数越界类 fuzzer 的典型目标。
运行不受信任模型的通用建议。SECURITY.md 还特别提醒:paddle.load隐式使用 pickle,恶意构造的模型文件可能导致任意代码执行,因此加载不可信模型应置于沙箱中;paddle.distributed的 RPC 等分布式能力不做加密与鉴权,仅应部署在可信网络内。
7. 防护与升级建议
基于以上分析,给开发者和使用者的实操建议如下:
- 升级到包含修复的版本。官方通告声明修复包含在PaddlePaddle 2.5.0中(修复提交
12549dfe3e87a4c30f852d2eca81d7f67c8daa87)。如果生产环境仍在使用 2.5.0 之前的版本,且存在"参数来源于外部输入"的paddle.trace调用路径,应优先升级。 - 在调用侧收紧参数类型与取值。即便在修复后的版本中,也建议对
axis1/axis2显式校验,例如确保-(ndim) <= axis < ndim且axis1 != axis2,并确认传入的是 Pythonint而非张量——这与 PoC 的输入形态形成对照。合法取值示例见 API 文档(math.py 示例):paddle.trace(case2, offset=1, axis1=1, axis2=2)。 - 把越界 offset 与越界 axis 区分开。
offset越界是合法语义(返回 0),业务代码可以依赖;axis1/axis2越界是非法输入,修复后的框架会抛出OutOfRange错误,业务侧应将其视为输入校验失败而非数值异常处理。 - 遵循沙箱与最小信任原则。若你的服务会把用户可控的脚本/模型参数传入张量 API(例如动态构图、自定义层拼装),应参照 SECURITY.md 的建议将执行环境沙箱化,并关注 security/advisory 目录下的后续通告。
8. 小结
PDSA-2023-003 是一个典型的"API 参数边界校验缺失"类漏洞:paddle.trace的 Python 动态图分支将axis1/axis2原样透传给 C++ 运行时,越界的巨大负数 axis 未经拦截即参与对角线维度推导,最终造成堆缓冲区溢出。修复方案将校验前置到 TraceInferMeta,用PADDLE_ENFORCE系列断言把非法输入在 Kernel 执行前干净地拒绝掉,与 PaddlePaddle 官方"非法输入应 clean exit 而非内存破坏"的安全模型完全对齐。对使用者而言,升级到 2.5.0+、在调用侧收紧 axis 参数、并将不受信任的执行隔离在沙箱中,是应对此类问题的完整防线。
【免费下载链接】PaddlePArallel Distributed Deep LEarning: Machine Learning Framework from Industrial Practice (『飞桨』核心框架,深度学习&机器学习高性能单机、分布式训练和跨平台部署)项目地址: https://gitcode.com/GitHub_Trending/pa/Paddle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考