编码红线

通用规则

1. 禁止硬编码敏感信息、使用禁用敏感词

  1. 源码硬编码有明文密码密钥等高级别敏感数据。
  2. 存在历史版本整改遗留的废弃账号和密码,后续代码交付如CSEC认证时,就有可能被客户质疑为隐藏的后门账号,风险很高,建议产品完全排查后进行彻底清理。
  3. 用途不明、数据定义含糊的大数组或者功能可读性差,容易被怀疑存在隐藏后门。《EWA-Canada安全认证介绍》也提出如此要求。
  4. 代码或者配置中硬编码一个公网IP地址、域名、邮箱地址、容易造成质疑的敏感词,如backdoor等,会造成非法收集回传客户数据的质疑,有后门之嫌。因此对于硬编码公网IP地址等要逐一排查,确认是否有合理解释。尤其不能包含指向华为或中国的ip地址。

2. 外部数据作为数组索引或者指针偏移时必须确保在地址大小范围内

数组索引直接由外部控制问题,根本原因在于内部分配的空间不足于支撑外部数据访问。因此若外部第三方通过通信接口访问内存相关的操作中,则应该注意是否存在越出数组以外场景,外部数据作为数组索引对内存进行访问时,必须对数据的大小进行严格的校验。

3. 整数之间运算时必须严格检查,确保不会出现溢出、反转、除0

整数问题归根结底是内存问题。因此若整数运算结果用于与内存相关的操作中,则应该注意整数是否会产生溢出/反转。

4. 内存或句柄为指针类型的资源释放之后应立即赋予新值,异常分支必须同步释放,避免导致资源泄露

资源泄露通常指:

  1. 文件操作句柄使用完后未关闭
  2. 内存使用完后未释放
  3. 数据库等使用完后未关闭

资源泄露会导致系统资源耗尽,造成DoS。此类问题多发生于异常分支、各种条件分支里。在编码过程中,需格外关注。

5. 内存申请前必须判断大小,申请后必须校验是否成功

  1. 内存申请的大小可能来自于外部数据,必须检查其合法性,防止过多地、非法地申请内存。不能申请0长度的内存。
  2. 外置allocator需要校验返回的结构体指针以及结构体内部的data指针是否都有效。

补充说明:外置allocator指用户可以通过 ge_api_v2.cc 中的接口注册的allocator,使用时没有区分,因此校验逻辑需要覆盖外置allocator返回结果。

6. 不要编写依赖参数求值顺序的代码(如func(a++, a)),这会导致未定义行为

在C/C++语言中,函数参数的求值顺序是未指定的,这意味着编译器可以以任意顺序对参数进行求值。因此,像func(a++, a)这样的代码,由于我们无法知道a++和a这两个参数哪一个先被求值,所以会导致未定义行为。

GE框架规则

1. 对外开放接口的预留参数要有严格校验,防止后续版本启用存在兼容性问题

预留参数需要强制用户使用无效值,否则后续开放使用后,之前用户随意填的数值在老版本上运行正常,而在新版本上运行异常,成为兼容性问题。譬如:指针类参数导致coredump(指针类),数值参数类导致逻辑错误。

补充说明:数值类预留参数的无效值一般是 0。指针类预留参数的无效值是 nullptr。

2. 对于对外接口的返回值或参数,内存长度的数据类型需定义为size_t类型

内存长度超过4G的场景下,32位无法表达,使用32位数据类型的话,遇到对应场景后,需要增加同样逻辑的新接口,造成多余的开发和维护成本。

3. 修改对外开放接口时,必须严格确保API和ABI兼容

inc/external目录下的头文件为对外开放的接口,需要严格确保ABI和API兼容性,如果不兼容,会导致客户代码修改或者重新编译,原有程序无法运行。

补充说明

  • inc/external 目录下的头文件即为全部对外接口的边界,其他目录视为内部接口,可以自由修改。
  • C++对外接口一般使用类(而非结构体),扩展时通过增加类的方法实现,不修改已有方法签名。
  • C对外结构体有预留字段。新建对外结构体时也必须预留字段。
  • 对外头文件的公开接口中禁止使用 std::string(包括函数参数、返回值、类/public 成员等),因为不同 C++ 标准库/编译器版本下 std::string 的底层实现可能不同,导致 ABI 不兼容。应使用 AscendString 代替。

4. 禁止改图时进行不等价转换,必须考虑控制、数据两类关系等价

GE的图优化正常是完全不改变图的计算结果的,在做这类优化时,要考虑"数据"、"控制"关系同时等价。

补充说明

  • 数据等价指输出张量一致。
  • 控制等价指控制边所约束的执行顺序不变。
  • 等价判定的范围是被修改的几个节点之间,不要求全局所有节点的执行顺序不变。

5. 禁止新增有时序依赖pass时不增加注释,必须说明依赖内容,并且备案申请

新增有时序依赖pass必须严格管控,原则上要做到无时序依赖,如果无法避免,需要申请备案,集中管理,并且按照要求增加注释说明。

补充说明:备案申请有两种方式:1)在 ge sig 例会上申请议题讨论;2)提 issue 讨论。当前没有独立的时序依赖pass记录文档,已有备案信息记录在各pass的代码注释中。

6. 禁止在代码中出现对芯片类型、前端框架类型、网络类型的硬编码

CANN作为异构计算框架,往上要对接各类框架,往下要支持多种硬件和芯片,为了保持框架能够onetrack演进,要和周边解耦,不允许感知芯片类型、算子类型(AICCore算子)、前端框架类型。

补充说明:芯片能力差异一般通过 aclrt 接口查询,不硬编码芯片类型判断。框架类型和网络类型对 GE 没有差异,不需要感知。

7. 禁止在插入节点时不考虑新插入节点与输入输出侧节点的控制边关系

向图上插入节点时,需要考虑插入的节点与前后节点的逻辑关系,如果插入节点与输入节点的关系紧密,需要将输入节点的输出控制边后移到插入的节点上,反之需要将输出节点的输入控制前移到插入的节点上。实际编码时,也可以使用封装好的接口GraphUtils::InsertNodeAfterGraphUtils::InsertNodeBefore

补充说明GraphUtils::InsertNodeAfterGraphUtils::InsertNodeBefore 已完整处理了控制边关系,不存在处理不了的场景。优先使用这两个接口,调用后无需再手动处理控制边。

8. 对节点名的唯一约束是:不可以重复

除了这个约束之外,理论上各个模块、pass可以任意修改节点名、删除本节点并新增一个同name的其他节点等等。

补充说明:需要定位特定节点时,应通过遍历图上节点 + 匹配条件的方式,条件可以是 OpType、节点属性等。

9. 禁止在加载/执行流程中增加EVENT/TRACE打印,防止产生海量日志,必须增加的需备案申请

plog直接写入DPC存储,DPC存储默认设置小IO聚合延迟下发,海量日志会导致小IO变慢,产生性能波动。

补充说明

  • 此限制仅针对加载/执行流程,编译/初始化等其他流程不限制。
  • 无论 Debug 还是 Release 构建,加载/执行流程中禁止增加 EVENT/TRACE 级别日志(默认开启会影响性能)。
  • 允许增加 INFO/DEBUG 级别日志(默认关闭,关闭后不影响性能)和 ERROR 级别日志。

10. 使用rtMemcpy/aclrtMemcpy/rtMemcpyAsync/aclrtMemcpyAsync时需确保拷贝动作发生时操作的内存有效

rtMemcpy/aclrtMemcpy是立即执行拷贝动作,需确保接口调用时操作的内存有效;rtMemcpyAsync/aclrtMemcpyAsync下发到指定的stream后立即返回,延迟调度,需确保该task被实际调度到加速器时操作的内存有效。

11. 使用rtMemcpy/aclrtMemcpy/rtMemcpyAsync/aclrtMemcpyAsync时,需要判断source size>0才进行拷贝,否则拷贝可能发生失败

调用拷贝时,如果size为0,表明原始的地址是一个nullptr,RTS接口会报错。

12. 使用rtMemcpyAsync/aclrtMemcpyAsync时,H2D的拷贝类型需要使用RT_MEMCPY_HOST_TO_DEVICE_EX/ACL_MEMCPY_HOST_TO_BUF_TO_DEVICE配置项,除非明确知道待拷贝的host内存不会被释放

带EX与不带EX的区别是,带EX选项的,RTS内部会把host内存做一次h2h拷贝,然后再做异步h2d,所以上层可以释放自己的host内存。不带EX选项的,底层不会做h2h拷贝,如果上层的host内存释放了,真正发生拷贝的时候会出现未定义行为。(带EX接口,性能会差一点)

13. 调用其他组件提供的需要GE传入资源相关接口时,需要相关组件明确该接口对传入资源是否有生命周期或释放时机的约束

在方案设计时也需要明确与其他组件之间接口是否有相关约束。例如:rtDevBinaryRegister持有了GE传入的指针,并且在context切换时重新使用该指针做H2D操作,导致GE必须保证注册二进制host内存在去注册之后才能释放。

补充说明:代码检视时识别跨组件接口生命周期约束的方法:

  • aclrt 开头的接口:下载 cann/runtime 代码,在 docs/ 目录下搜索文档,该目录下都是 aclrt 开头的接口文档。
  • rt 开头的接口:在 pkg_inc/runtime/ 目录下找到接口原型。rt 开头的接口没有文档,需要阅读源码判断,或者在 cann/runtime 仓提 issue 询问。

14. 对图做增删改操作时,禁止使用std::unordered_map等无序容器或使用Node指针作为key的std::map进行存储和遍历,确保多次处理结果一致

使用std::unordered_map等无序容器,虽然存储在里面的key是一致的,但是存储顺序无法保证一致,遍历该容器进行改图处理时可能会导致在图上插入节点的顺序不一致,如果图上有对执行顺序有严格要求的算子(通信算子),多P场景执行时可能引发卡死问题。同样使用Node指针进行存储也无法保证存储顺序的一致性(比如Node1和Node2的指针顺序在多次执行时可能会发生变化)。

补充说明

  • 推荐使用有序集合(如 std::map),可用节点名作为 key。
  • 禁止使用 std::unordered_map 等无序容器。
  • 禁止使用 Node 指针作为 key:每个进程的指针地址不同,导致遍历顺序不一致,多次执行结果可能不同。
  • 使用节点名作为 key 时,只能在一个局部函数中临时存储,函数结束后销毁。不要跨函数或长期持有以节点名为 key 的容器。

15. 禁止单例模式在头文件中以内联方式实现

单例模式在头文件中以内联方式实现可能会有如下问题:

  • 每个包含该头文件的 .cc 文件(编译单元)都会生成一份该函数的本地副本,可能生成多个实例问题(核心隐患)
  • 虽然 C++ 标准规定 static 局部变量在同一个程序中应该只初始化一次,但在动态库(dlopen)场景下,这个保证可能被打破,不同的 SO(共享库)可能各自持有一个实例
  • 可能出现dlopen 加载顺序导致的 Coredump

16. 禁止使用非开放的RTS接口

对于rt开头的接口只能使用pkg_inc/runtime/rt_external*.h头文件里的,不在这些头文件里的接口禁止使用,需要用aclrt开头的等价接口。

17. 禁止在静态对象/全局对象析构函数里做跨so的函数调用

静态对象在跨动态库(.so)调用时,析构过程存在几类典型风险,根源在于 C++ 标准对不同编译单元(包括动态库)中静态对象的析构顺序不作保证。

风险类型

  • 析构顺序未定义导致悬空引用:程序退出时,全局/静态对象按"构造逆序"析构,但仅在同一编译单元有效。不同.so的析构顺序由链接器决定,不可预测。若.so A的静态对象析构时访问.so B已析构的对象,产生悬空引用导致崩溃。
  • 析构函数中抛出异常导致程序终止:跨.so析构链中,若某对象因依赖失效抛异常且未捕获,直接std::terminate()。
  • 多线程竞争:程序退出时工作线程可能访问正在析构的静态对象。
  • 动态库卸载不确定性:运行期卸载.so时,静态对象析构时机不可控。

典型违规场景: 静态对象持有跨.so对象:

// 违规:静态单例析构时,持有的shared_ptr指向其他.so的对象
class ExecutorManager {
public:
  static ExecutorManager& GetInstance() {
    static ExecutorManager instance;  // 静态对象
    return instance;
  }
private:
  std::map<std::string, std::shared_ptr<Executor>> executors_;
  // Executor对象在plugin.so中,析构时.so可能已卸载 → 崩溃
};

正确做法: 清理工作在Finalize()中完成,析构函数不清理跨.so对象:

void ExecutorManager::Finalize() {
  executors_.clear();  // 运行时清理,.so未卸载
}

ExecutorManager::~ExecutorManager() = default;  // 析构时executors_已空

shared_ptr陷阱

  • shared_ptr析构时调用虚析构函数,虚析构函数在对象实际类型的.so中定义
  • 若.so已卸载,调用虚析构函数访问已释放内存 → 崩溃
  • shared_ptr不能延迟析构到程序退出阶段

常见误报

场景 原因
void* + mmDlclose 裸指针不触发析构,mmDlclose是编译链接
有Finalize且上层调用 Finalize主动清理,析构防御性空操作
runtime调用aclrt* libascendcl.so编译链接,卸载顺序有保证
同.so静态注册 同编译单元,无跨.so风险