已关闭
【runtime】TPRT-002: 源缓冲区大小未校验导致越界读 #822
佳祺创建于  26 天前关闭于  2 天前
佳祺
佳祺
26 天前 创建

【runtime】TPRT-002: 源缓冲区大小未校验导致越界读

基本信息

属性
漏洞 ID TPRT-002
类型 buffer-over-read (CWE-125: Out-of-bounds Read)
严重性 High
置信度 CONFIRMED
文件 src/tprt/feature/src/tprt_sqhandle.cc
函数 SqPushTask (第69-96行)
模块 tprt (Task Processing Runtime Transport)

漏洞描述

tprt_sqhandle.cc 中的 SqPushTask 函数根据用户提供的 sqeNum 值,从用户提供的 sqeAddr 缓冲区循环拷贝 SQE 条目到内部队列 sqQueue_。整个路径上仅校验了内部队列容量是否充足IsQueueFull),从未验证 sqeAddr 指向的源缓冲区是否实际包含 sqeNumTprtSqe_t 元素。若攻击者声明较大的 sqeNum 但提供较小的源缓冲区,将触发越界读,泄漏的内存数据被写入内部队列并可能被后续执行。

漏洞代码

// tprt_sqhandle.cc:69-96
uint32_t TprtSqHandle::SqPushTask(const uint8_t* sqeAddr, const uint32_t sqeNum)
{
    const uint32_t depth = TprtManage::Instance()->TprtGetSqMaxDepth();
    const std::lock_guard<std::mutex> lock(sqQueueLock_);
    uint16_t sqHead = sqHead_.load();
    uint16_t sqTail = sqTail_.load();
    bool queueFull = TprtManage::Instance()->IsQueueFull(sqHead, sqTail, sqeNum);
    if (queueFull) {
        // ... 仅检查内部队列是否满 ...
        return TPRT_SQ_QUEUE_FULL;
    }
    uint32_t copySqeNum = 0U;
    while (copySqeNum != sqeNum) {
        // ❌ 将 sqeAddr 强制转换为 TprtSqe_t* 并按索引读取
        // ❌ 未验证 sqeAddr 缓冲区是否有 sqeNum 个元素
        sqQueue_[sqTail] = (TprtPtrToPtr<const TprtSqe_t*>(sqeAddr))[copySqeNum];
        PrintTprtSqe(&sqQueue_[sqTail]);
        ++copySqeNum;
        sqTail = (sqTail + 1U) % depth;
    }
    sqTail_.store(sqTail);
    return TPRT_SUCCESS;
}

TprtPtrToPtr 的实现(tprt_base.hpp:95-99)——纯 reinterpret_cast,无安全检查:

template <typename TO, typename TI>
inline TO TprtPtrToPtr(const TI ptr)
{
    return reinterpret_cast<TO>(ptr);
}

完整数据流分析

攻击路径

攻击者构造 TprtTaskSendInfo_t
    │  sqeAddr = 小缓冲区(如仅 1 个 TprtSqe_t)
    │  sqeNum  = 大数值(如 100,小于 depth=1024)
    ▼
TprtSqPushTask(devId, sendInfo)          [tprt_api.cc:119] ← 公开 C API
    │  验证: sendInfo != NULL ✓
    │  验证: sendInfo->sqeNum < depth ✓
    │  ❌ 不验证 sendInfo->sqeAddr 是否为 NULL
    │  ❌ 不验证 sendInfo->sqeAddr 缓冲区实际大小
    ▼
SqPushTask(sqeAddr, sqeNum)              [tprt_sqhandle.cc:69]
    │  验证: IsQueueFull(sqHead, sqTail, sqeNum) ✓ (仅检查目标队列容量)
    │  ❌ 不验证源缓冲区 sqeAddr 的大小
    │
    │  while (copySqeNum != sqeNum):
    │    sqQueue_[sqTail] = (TprtSqe_t*)sqeAddr)[copySqeNum]
    │    ↑ 当 copySqeNum >= 实际元素数时 → 越界读
    ▼
越界读取的内存数据写入 sqQueue_ 内部队列
    ▼
WorkerWakeUp()                           [tprt_api.cc:147]
    ▼
SqExeTask → TprtExeAicpuTask            [tprt_sqhandle.cc:47]
    │  泄漏数据被当作合法 SQE 执行
    │  → 可能触发 TPRT-001 链式利用(任意代码执行)

关键数据结构

// tprt_type.h:34-38 — 用户提交的发送信息结构体
typedef struct tagTprtTaskSendInfo_t {
    uint8_t* sqeAddr;     // 用户控制的 SQE 缓冲区地址
    uint32_t sqId;        // 提交队列 ID
    uint32_t sqeNum;      // 声称的 SQE 数量 ← 攻击者控制
} TprtTaskSendInfo_t;

// tprt_sqe_cqe.h:73-76 — SQE 结构体(packed,约 66 字节)
union TprtSqe_t {
    TprtStarsCommonSqe_t commonSqe;   // header(10B) + commandCustom[14](56B) = 66B
    TprtStarsAicpuSqe aicpuSqe;       // header(10B) + addrs + timeout + res = 66B
};

每个 TprtSqe_t 约 66 字节(#pragma pack(1)),若 sqeNum=100 但缓冲区仅含 1 个元素,越界读范围为 (100-1) × 66 = 6534 字节。

API 入口点验证缺陷分析

TprtSqPushTasktprt_api.cc:119-149)执行的验证:

验证项 状态 代码位置
sendInfo != nullptr ✅ 存在 第122行
sendInfo->sqeNum < depth ✅ 存在 第122行
devId 有效性 ✅ 存在 第127-131行
sendInfo->sqId 有效性 ✅ 存在 第132-136行
worker 有效性 ✅ 存在 第137-141行
sendInfo->sqeAddr != nullptr ❌ 缺失
sendInfo->sqeAddr 缓冲区大小 ≥ sqeNum × sizeof(TprtSqe_t) ❌ 缺失
sendInfo->sqeNum > 0 ❌ 缺失

SqPushTask 内部验证:

验证项 状态 说明
队列满检查 (IsQueueFull) ✅ 存在 仅保护目标队列不溢出
源缓冲区大小检查 ❌ 缺失 核心缺陷
sqeAddr 空指针检查 ❌ 缺失 传入 NULL 将导致段错误

攻击面分析

入口点

TprtSqPushTask 声明于 src/tprt/inc/external/tprt_api.h,以 extern "C" 导出,是公开 C API:

// tprt_api.h:21
uint32_t TprtSqPushTask(const uint32_t devId, const TprtTaskSendInfo_t* sendInfo);

攻击前提

  1. 攻击者进程能够链接到 tprt 库(libtprt.so
  2. 攻击者已通过 TprtDeviceOpenTprtSqCqCreate 创建了设备和 SQ/CQ
  3. 攻击者构造 TprtTaskSendInfo_t
    • sqeAddr = 指向一个仅含 N 个 TprtSqe_t 的缓冲区
    • sqeNum = M,其中 M > N 且 M < depth(最大 1024)
    • sqId = 已创建的合法 SQ ID

攻击示例

// 攻击者代码
TprtSqe_t smallBuf[1];  // 仅分配 1 个 SQE 的空间(~66 字节)
memset(smallBuf, 0, sizeof(smallBuf));

TprtTaskSendInfo_t sendInfo = {
    .sqeAddr = (uint8_t*)smallBuf,
    .sqId = 0,
    .sqeNum = 100   // 声称有 100 个 SQE,实际只有 1 个
};

// 将触发 99 × 66 = 6534 字节的越界读
TprtSqPushTask(devId, &sendInfo);

影响评估

直接影响

  • 信息泄露: 越界读取的内存内容被拷贝到 sqQueue_ 内部队列中,可能包含敏感数据(密钥、指针、其他进程数据)
  • 拒绝服务: 若 sqeAddr 为 NULL 或越界读触及未映射页面,导致进程崩溃(段错误)
  • 数据损坏: 泄漏的内存数据被当作合法 SQE 写入内部队列,破坏队列状态

链式利用(与 TPRT-001 组合)

越界读泄漏的数据若恰好包含可解释为 startPcAddrargsAddr 的值,将被 SqExeTaskTprtExeAicpuTask 当作函数指针执行(参见 TPRT-001),形成信息泄露 → 任意代码执行的链式攻击:

TPRT-002 (越界读) → 泄漏数据写入 sqQueue_
    → SqExeTask 将泄漏数据解释为 SQE
    → TPRT-001 (不可信指针解引用) → 任意代码执行

修复建议

方案一:在 API 入口增加源缓冲区大小验证(推荐)

TprtSqPushTask 中增加对 sqeAddr 的验证:

uint32_t TprtSqPushTask(const uint32_t devId, const TprtTaskSendInfo_t* sendInfo)
{
    const uint32_t depth = cce::tprt::TprtManage::Instance()->TprtGetSqMaxDepth();
    if ((sendInfo == nullptr) || (sendInfo->sqeNum == 0U) || (sendInfo->sqeNum >= depth)) {
        TPRT_LOG(TPRT_LOG_ERROR, "input is invalid device_id[%u].", devId);
        return TPRT_INPUT_INVALID;
    }
    // 新增:验证 sqeAddr 非空
    if (sendInfo->sqeAddr == nullptr) {
        TPRT_LOG(TPRT_LOG_ERROR, "sqeAddr is null, device_id[%u].", devId);
        return TPRT_INPUT_INVALID;
    }
    // ... 后续逻辑不变 ...
}

注意: C API 层面无法直接验证缓冲区实际大小(缺少 size 参数),这是 API 设计的根本缺陷。方案一仅能防御 NULL 指针和 sqeNum=0 的边界情况。

方案二:扩展 API 接口增加缓冲区大小参数(根本修复)

修改 TprtTaskSendInfo_t 结构体,增加缓冲区大小字段:

typedef struct tagTprtTaskSendInfo_t {
    uint8_t* sqeAddr;
    uint32_t sqId;
    uint32_t sqeNum;
    uint32_t sqeBufSize;  // 新增:缓冲区实际字节大小
} TprtTaskSendInfo_t;

SqPushTask 中验证:

uint32_t TprtSqHandle::SqPushTask(const uint8_t* sqeAddr, const uint32_t sqeNum,
                                   const uint32_t sqeBufSize)
{
    if (sqeAddr == nullptr || sqeNum == 0U) {
        return TPRT_INPUT_INVALID;
    }
    const uint32_t requiredSize = sqeNum * static_cast<uint32_t>(sizeof(TprtSqe_t));
    if (sqeBufSize < requiredSize) {
        TPRT_LOG(TPRT_LOG_ERROR,
                 "sqeBufSize[%u] < required[%u] (sqeNum=%u, sizeof(TprtSqe_t)=%zu)",
                 sqeBufSize, requiredSize, sqeNum, sizeof(TprtSqe_t));
        return TPRT_INPUT_INVALID;
    }
    // ... 后续拷贝逻辑不变 ...
}

方案三:防御性拷贝(缓解措施)

若无法修改 API,可在 SqPushTask 中使用安全拷贝函数,在读取每个 SQE 前验证源地址可读性:

while (copySqeNum != sqeNum) {
    const TprtSqe_t* src = TprtPtrToPtr<const TprtSqe_t*>(sqeAddr) + copySqeNum;
    // 使用平台相关的安全内存读取检查
    if (!IsMemoryReadable(src, sizeof(TprtSqe_t))) {
        TPRT_LOG(TPRT_LOG_ERROR, "sqeAddr read failed at index %u", copySqeNum);
        return TPRT_INPUT_INVALID;
    }
    sqQueue_[sqTail] = *src;
    ++copySqeNum;
    sqTail = (sqTail + 1U) % depth;
}

验证方法

  1. 单元测试: 构造 TprtTaskSendInfo_t,设置 sqeNum 大于 sqeAddr 实际元素数,使用 AddressSanitizer 编译运行,验证是否触发 OOB read 报告
  2. NULL 指针测试: 设置 sqeAddr = NULLsqeNum = 1,验证是否被正确拒绝
  3. 边界测试: 设置 sqeNum = 0,验证不会进入异常路径
  4. 模糊测试: 对 sqeNumsqeAddr 进行随机组合测试,监控越界访问
likedislike
ykl999
ykl999成员
26 天前 评论:

/assign @Reyn52166

likedislike
CANN-robotCANN-robot成员
26 天前 将 Reyn52166 设为负责人
ykl999
ykl999成员
26 天前 评论:

你好该问题已收到,针对 源缓冲区大小未校验导致越界读这个问题,我们将根据issue提出代码检查下

likedislike
Lleihuan1成员
25 天前 将 guo-yanjun 设为负责人
guo-yanjun成员
12 天前 评论:

@syaunfang 感谢反馈。针对该问题,我们已完成初步分析并形成统一整改方案:计划将 libxpu_tprt.so 调整为仅构建期使用的静态库,内部链接到 Runtime v100/v200,同时隐藏 Tprt*、cce::tprt::*等动态符号,并清理相关安装及打包清单,确保交付包不再包含 libxpu_tprt.so 或 libxpu_tprt.a。
该方案将收口 TprtCqReportRecv、TprtSqPushTask 等内部接口通过动态链接或 dlsym 被外部直接调用的路径,并保留现有 Runtime 内部调用链。后续还将补充必要的输入健壮性检查,并完成动态依赖、符号表、安装包、功能回归及升级回滚验证。目前正在修改代码并进行依赖和兼容性确认,有进一步进展会及时同步。

likedislike
guo-yanjun成员
2 天前 评论:

@syaunfang 您好,TPRT 内部接口暴露问题已完成整改和验证,相关修改已提交代码仓,如果您后续有新的进展或诉求,欢迎随时重新开启此 Issue 或提交新的 Issue,我们会继续为您跟进。

likedislike
Gguo-yanjun成员
2 天前 issue状态由 待办的 改变为 已完成
Gguo-yanjun成员
2 天前 关闭了 issue
CANN-robotCANN-robot成员
2 天前 添加了label:resolved