☰
华为CC++安全编程规范实战:从缓冲区溢出到可落地检查清单
2026/10/15 12:46:58 网站建设 项目流程

简介:《华为C&C++语言安全编程规范-V3.1》面向C/C++开发人员与安全编码学习者,旨在帮助编程者建立攻击者思维、养成安全编码习惯,从源头降低缓冲区溢出、整数溢出、注入等常见安全风险。规范围绕基础要求、字符串与数组操作、安全函数使用、整数运算、内存管理、不安全函数、文件输入输出及敏感信息处理等模块展开,逐条给出规则与建议,并附危险函数替换列表与异步安全函数清单,可作为日常编码自查与团队规范落地的参考依据。资源包共1个PDF文件,压缩包约911KB,内容为完整规范正文,目录层级清晰,便于按章节检索查阅。目前已有1175人学习下载,适合希望系统梳理C/C++安全编码要点、对照检查既有代码的开发者阅读。

1. 安全编程规范不是文档摆设:从一次缓冲区溢出说起

很多做 C/C++ 的同行都有一个习惯:拿到一份安全编程规范,翻两页觉得“这不就是编码风格指南”,然后扔进收藏夹吃灰。直到某天线上服务在凌晨三点崩了,core dump 拉出来一看,一个strcpy把 256 字节的栈缓冲区写爆了,攻击者构造的输入直接覆盖了返回地址。这种事故不是玄学,是规范里白纸黑字写过的坑。

华为 C&C++ 语言安全编程规范 V3.1 就是针对这类问题的系统性约束集合,覆盖内存管理、字符串操作、整数运算、并发、文件 IO 等高风险区域。它解决的核心问题是:在 C/C++ 这种没有内存安全兜底的语言里,如何通过编码约束把常见漏洞面收敛到最小。适合谁看?写服务端、嵌入式、协议栈、驱动这类底层代码的工程师,尤其是代码要过安全审计、要过静态扫描工具、要交付给第三方做渗透测试的团队。这篇笔记不讲空话,把规范里最要命的几类条款拆成能直接落地的做法。

2. 内存与字符串:规范里最容易翻车的两块

2.1 为什么strcpy/sprintf是重灾区

C 标准库的字符串函数几乎都不做边界检查。strcpy(dst, src)只管拷到\0为止,sprintf只管格式化到缓冲区满为止。规范里明确要求:禁止使用无边界检查的字符串函数,必须用带长度参数的版本。

常见做法是替换成strncpy、snprintf,但这里有个血泪经验:strncpy不保证结尾有\0。如果源串长度大于等于目标缓冲区大小,strncpy拷满 n 个字节后不会补\0,后续任何字符串操作都会越界读。正确姿势是手动补零,或者用更安全的封装。

#include <stdio.h> #include <string.h> #define DST_SIZE 64 int safe_copy(char *dst, size_t dst_size, const char *src) { if (dst == NULL || src == NULL || dst_size == 0) { return -1; // 参数校验,规范要求所有外部输入必须校验 } size_t src_len = strlen(src); if (src_len >= dst_size) { return -1; // 源串过长,直接拒绝,不做截断 } memcpy(dst, src, src_len); dst[src_len] = '\0'; // 显式补终止符,这是 strncpy 容易漏的一步 return 0; } int main(void) { char buf[DST_SIZE]; if (safe_copy(buf, sizeof(buf), "hello") != 0) { printf("copy failed\n"); return 1; } printf("%s\n", buf); return 0; }

这段代码的逻辑说明:先做空指针和长度校验,再用strlen判断源串是否放得下,放不下直接返回错误而不是截断。参数说明:dst_size必须传sizeof(buf)而不是硬编码数字,避免缓冲区大小改了但长度参数没改的经典错误。memcpy比strncpy更可控,因为长度已经提前算好了。

2.2 动态内存的申请与释放配对

规范里对malloc/free的要求可以总结成三条:谁申请谁释放、释放后指针置 NULL、禁止释放非堆指针。听起来简单,但在多分支函数里极容易漏。

#include <stdlib.h> #include <string.h> char *build_message(const char *body, size_t body_len) { if (body == NULL || body_len == 0) { return NULL; } // 多申请 1 字节给终止符,规范要求长度计算必须考虑溢出 if (body_len > SIZE_MAX - 1) { return NULL; // 防止 body_len + 1 溢出 } char *msg = (char *)malloc(body_len + 1); if (msg == NULL) { return NULL; } memcpy(msg, body, body_len); msg[body_len] = '\0'; return msg; // 调用方负责 free } void use_message(void) { char *m = build_message("data", 4); if (m != NULL) { // 使用 m free(m); m = NULL; // 释放后置空,防止悬空指针 } }

逻辑说明:长度计算前先判断body_len + 1是否会溢出size_t,这是规范里整数溢出防护的典型条款。参数说明:SIZE_MAX来自<stdint.h>,在 32 位平台是 4G-1,64 位平台更大。释放后置 NULL 不是形式主义,是为了让后续的if (m != NULL)判断真正生效。

2.3 整数溢出:被忽视的漏洞入口

整数溢出在 C/C++ 里是未定义行为(有符号)或回绕(无符号),规范要求所有参与内存分配、数组索引、循环边界的整数运算都必须做溢出检查。常见做法是用__builtin_add_overflow这类编译器内建函数,或者手动判断边界。

#include <stdint.h> #include <stddef.h> int safe_array_index(size_t base, size_t offset, size_t total, size_t *out) { if (out == NULL) { return -1; } // 检查 base + offset 是否溢出,以及是否越界 if (offset > total || base > total - offset) { return -1; } *out = base + offset; return 0; }

逻辑说明:把base + offset <= total改写成base <= total - offset,避免加法本身溢出。参数说明:total是数组元素总数,base和offset都必须是已经校验过非负的值。这个写法在规范里属于“用减法代替加法做边界判断”的标准套路。

3. 并发与资源管理:多线程下的规范落地

3.1 锁的获取与释放必须成对且异常安全

规范要求:禁止在持有锁的情况下调用可能阻塞或回调外部代码的函数,禁止锁未释放的所有返回路径。C 语言没有 RAII,只能靠代码结构保证。

#include <pthread.h> static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static int g_counter = 0; int increment_counter(void) { if (pthread_mutex_lock(&g_lock) != 0) { return -1; } // 临界区内只做纯内存操作,不调用外部函数 g_counter++; if (pthread_mutex_unlock(&g_lock) != 0) { return -1; // 解锁失败属于严重错误,但不能再重试 } return 0; }

逻辑说明:加锁后立刻进入临界区,临界区内只有一条自增语句,没有任何可能失败或阻塞的调用。参数说明:pthread_mutex_lock返回 0 表示成功,非 0 直接返回错误,不继续执行。解锁失败通常意味着锁状态已损坏,继续操作没有意义。

3.2 文件描述符与句柄的释放顺序

规范里有一条容易被忽略:打开多个资源时,释放顺序必须与打开顺序相反,且每个资源在打开后立即判断是否成功。常见翻车场景是打开了文件 A 和 B,B 打开失败直接 return,忘了关 A。

#include <stdio.h> int process_two_files(const char *path_a, const char *path_b) { FILE *fa = NULL; FILE *fb = NULL; int ret = -1; fa = fopen(path_a, "r"); if (fa == NULL) { goto cleanup; } fb = fopen(path_b, "r"); if (fb == NULL) { goto cleanup; } // 正常处理逻辑 ret = 0; cleanup: if (fb != NULL) { fclose(fb); fb = NULL; } if (fa != NULL) { fclose(fa); fa = NULL; } return ret; }

逻辑说明:用goto cleanup统一收口所有失败路径,保证每个已打开的资源都被释放。参数说明:goto在 C 里做资源清理是规范允许甚至推荐的模式,但禁止用它做向前跳转的业务逻辑。释放顺序与打开顺序相反,避免依赖关系出错。

3.3 信号处理函数里能做什么

规范对信号处理函数的约束非常严格:只能调用异步信号安全的函数,禁止malloc、free、printf、加锁等操作。常见做法是在信号处理函数里只设置一个volatile sig_atomic_t标志,主循环轮询该标志。

#include <signal.h> #include <unistd.h> static volatile sig_atomic_t g_stop = 0; static void on_signal(int sig) { (void)sig; g_stop = 1; // 只做赋值,异步信号安全 } int main(void) { struct sigaction sa; sa.sa_handler = on_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) != 0) { return 1; } while (!g_stop) { // 主循环做实际工作 usleep(100000); } return 0; }

逻辑说明:信号处理函数只修改volatile sig_atomic_t变量,这是标准保证的原子操作。参数说明:sa_flags设为 0 表示不使用SA_RESTART,这样阻塞的系统调用会被信号中断,主循环能及时看到标志变化。

4. 避坑与排查:规范落地时最常见的五个翻车点

4.1 用了snprintf但返回值没判断

现象:代码里写了snprintf(buf, sizeof(buf), fmt, ...),看起来安全,但格式化结果被截断了,后续逻辑拿到的是不完整字符串。原因:snprintf返回的是“假如缓冲区足够大时应该写入的字符数”,不是实际写入数。如果返回值大于等于缓冲区大小,说明发生了截断。解决:判断返回值,截断时走错误分支。

int n = snprintf(buf, sizeof(buf), "%s-%s", a, b); if (n < 0 || (size_t)n >= sizeof(buf)) { return -1; // 格式化失败或截断 }

4.2free之后继续使用指针

现象:程序偶发崩溃,堆栈指向一个已经释放的结构体。原因:free(p)之后p本身的值没变,仍然指向已回收的内存,后续读写就是 use-after-free。解决:释放后立即p = NULL,并且所有持有该指针的副本都要清理。更彻底的做法是封装释放函数,传二级指针。

4.3 有符号整数比较被隐式转换

现象:if (len < sizeof(buf))判断通过,但实际发生了越界。原因:len是int,sizeof(buf)是size_t(无符号),比较时len被转成无符号,负数变成巨大值。解决:统一用size_t或显式转换,规范要求禁止有符号与无符号直接比较。

4.4 锁的粒度太大导致死锁

现象:两个线程互相等待对方持有的锁,服务卡死。原因:在持锁状态下调用了另一个会加锁的函数,且加锁顺序不一致。解决:规范要求所有加锁路径必须按固定顺序,或者用trylock加超时。更常见的做法是把锁粒度缩小到只保护共享数据本身。

4.5 静态扫描工具报了一堆但没人改

现象:CI 里接了静态扫描,每次构建几百条告警,团队直接忽略。原因:没有做告警分级和基线管理,历史代码的告警淹没了新增代码的问题。解决:先跑一遍全量扫描,把存量告警加入白名单,只对增量代码做强制门禁。规范落地不是一次清理,是持续约束。

5. 把规范变成可执行的检查清单

规范文本再厚,不落到工具链里就是废纸。我一般会做三件事:第一,把规范里“禁止”“必须”的条款逐条映射到静态扫描规则,比如禁止strcpy就配一条正则或 AST 规则;第二,在代码评审模板里加一个安全自查项,每次 MR 必须勾选;第三,对高风险模块做定向的模糊测试,用输入验证边界检查是否真的生效。

下面这张表是我常用的规范条款与工具映射关系,可以直接抄:

规范条款检查手段工具类型误报处理
禁止无边界字符串函数AST 匹配函数名静态扫描白名单封装函数
内存释放后置 NULL数据流分析静态扫描手动标注
整数运算溢出检查区间分析静态扫描人工确认
锁的成对释放控制流分析静态扫描异常路径标注
外部输入校验污点分析动态+静态输入源标注

验证方法上,我习惯用一个小技巧:写一个故意违反规范的测试文件,跑一遍扫描工具,看它能不能全部报出来。如果漏报,说明规则没配全;如果误报太多,说明规则太激进。这个“自检用例”应该跟规范一起维护,规范更新了,自检用例也要更新。

最后一个习惯:每次修完一个安全漏洞,回头查一下规范里对应哪一条,然后把这条规范在团队内做一次五分钟的分享。漏洞是最好的教材,比读十遍规范都管用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询