底软基础 | 嵌入式程序员编程必看的525钟C/C++ 安全编程问题

《360 安全规则集合》简称《安规集》,是一套详细的 C/C++ 安全编程指南,由 360 集团质量工程部编著,将编程时需要注意的问题总结成若干规则,可为制定编程规范提供依据,也可为代码审计或相关培训提供指导意见,旨在提升软件产品的可靠性、健壮性、可移植性以及可维护性,从而提升软件产品的综合安全性能。

  • 原文链接:https://saferules.github.io/

C/C++ 安全规则集合

[外链图片转存中…(img-z2rxUD7I-1720615338134)]

Bjarne Stroustrup: “C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.

  针对 C 和 C++ 语言,本文收录了 525 种需要重点关注的问题,可为制定编程规范提供依据,也可为代码审计以及相关培训提供指导意见,适用于桌面、服务端以及嵌入式等软件系统。
  每个问题对应一条规则,每条规则可直接作为规范条款或审计检查点,本文是适用于不同应用场景的规则集合,读者可根据自身需求从中选取某个子集作为规范或审计依据,从而提高软件产品的安全性。

规则说明

规则按如下主题分为 17 个类别:

  1. Security:敏感信息防护与安全策略
  2. Resource:资源管理
  3. Precompile:预处理、宏、注释、文件
  4. Global:全局及命名空间作用域
  5. Type:类型设计与实现
  6. Declaration:声明
  7. Exception:异常
  8. Function:函数实现
  9. Control:流程控制
  10. Expression:表达式
  11. Literal:字面常量
  12. Cast:类型转换
  13. Buffer:缓冲区
  14. Pointer:指针
  15. Interruption:中断与信号处理
  16. Concurrency:异步与并发
  17. Style:样式与风格

每条规则包括:

  • 编号:规则在本文中的章节编号,以“R”开头,称为 Section-ID
  • 名称:用简练的短语描述违反规则的状况,以“ID_”开头,称为 Fault-ID
  • 标题:规则的定义
  • 说明:规则设立的原因、违反规则的后果、示例、改进建议、参照依据、参考资料等内容

如果违反规则,后果的严重程度分为:

  • Error:直接导致错误或形成安全漏洞
  • Warning:可导致错误或形成安全隐患
  • Suspicious:可疑的代码,需进一步排查
  • Suggestion:代码质量降低,应依照建议改进

规则的说明包含:

  • 示例:规则相关的示例代码,指明符合规则(Compliant)的和违反规则(Non-compliant)的情况
  • 相关:与当前规则有相关性的规则,可作为扩展阅读的线索
  • 依据:规则依照的 ISO/IEC 标准条目,C 规则以 ISO/IEC 9899:2011 为主,C++ 规则以 ISO/IEC 14882:2011 为主
  • 配置:某些规则的细节可灵活设置,审计工具可以此为参照实现定制化功能
  • 参考:规则参考的其他规范条目,如 C++ Core Guidelines、MISRA、SEI CERT Coding Standards 等,也可作为扩展阅读的线索

规则的相关性分为:

  • 特化:设规则 A 的特殊情况需要由规则 B 阐明,称规则 B 是规则 A 的特化
  • 泛化:与特化相反,称规则 A 是规则 B 的泛化
  • 相交:设两个规则针对不同的问题,但在内容上有一定的交集,称这两个规则相交

规则以“标准名称:版本 章节编号(段落编号)-性质”的格式引用标准,如“ISO/IEC 14882:2011 5.6(4)-undefined”,表示引用 C++11 标准的第 5 章第 6 节第 4 段说明的具有 undefined 性质的问题。

其中“性质”分为:

  • undefined:可使程序产生未定义的行为,这种行为造成的后果是不可预期的
  • unspecified:可使程序产生未声明的行为,这种行为由编译器或环境定义,具有随意性
  • implementation:可使程序产生由实现定义的行为,这种行为由编译器或环境定义,有明确的文档支持
  • deprecated:已被废弃的或不建议继续使用的编程方式

本文以 ISO/IEC 9899:2011、ISO/IEC 14882:2011 为主要依据,兼顾 C++17 以及历史标准,没有特殊说明的规则同时适用于 C 语言和 C++ 语言,只适用于某一种语言的规则会另有说明。

规则选取

本文是适用于不同应用场景的规则集合,读者可选取适合自己需求的规则。

指出某种错误的规则,如有“不可”、“不应”等字样的规则应尽量被选取,有“禁用”等字样的规则可能只适用于某一场景,可酌情选取。

如果将本文作为培训内容,为了全面理解各种场景下存在的问题,应选取全部规则。

规则列表

1. Security

2. Resource

3. Precompile

4. Global

5. Type

6. Declaration

7. Exception

8. Function

9. Control

10. Expression

11. Literal

12. Cast

13. Buffer

14. Pointer

15. Interruption

16. Concurrency

17. Style

1. Security

▌R1.1 敏感数据不可写入代码

ID_plainSensitiveInfo       🛡 security warning


代码中的敏感数据极易泄露,产品及相关运维、测试工具的代码均不可记录任何敏感数据。

示例:

/**
 * My name is Rabbit
 * My passphrase is Y2Fycm90         // Non-compliant
 */

#define PASSWORD "Y2Fycm90"          // Non-compliant

const char* passcode = "Y2Fycm90";   // Non-compliant

将密码等敏感数据写入代码是非常不安全的,即使例中 Y2Fycm90 是实际密码的某种变换,聪明的读者也会很快将其破解。

敏感数据的界定是产品设计的重要环节。对具有高可靠性要求的客户端软件,不建议保存任何敏感数据,对于必须保存敏感数据的软件系统,则需要落实安全的存储机制以及相关的评审与测试。



相关

ID_secretLeak

参考

CWE-259
CWE-798
SEI CERT MSC41-C



▌R1.2 敏感数据不可被系统外界感知

ID_secretLeak       🛡 security warning


敏感数据出入软件系统时需采用有效的保护措施。

示例:

void foo(User* u) {
    log("username: %s, password: %s", u->name, u->pw);   // Non-compliant
}

显然,将敏感数据直接输出到界面、日志或其他外界可感知的介质中是不安全的,需避免敏感数据的有意外传,除此之外,还需要落实具体的保护措施。

保护措施包括但不限于:

  • 避免用明文或弱加密方式传输敏感数据
  • 避免敏感数据从内存交换到外存
  • 避免敏感数据写入内存转储文件
  • 应具备反调试机制,使外界无法获得程序的内部数据
  • 应具备反注入机制,使外界无法篡改程序的行为

下面以 Windows 平台为例,给出阻止敏感数据从内存交换到外存的示例:

class SecretBuf {
    size_t len = 0;
    unsigned char* buf = nullptr;

public:
    SecretBuf(size_t size) {
        auto* tmp = (unsigned char*)VirtualAlloc(
            0, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE
        );
        if (VirtualLock(tmp, size)) {   // The key point
            buf = tmp;
            len = size;
        } else {
            VirtualFree(tmp, 0, MEM_RELEASE);
        }
    }

   ~SecretBuf() {
        SecureZeroMemory(buf, len);   // Clear the secret content
        VirtualUnlock(buf, len);
        VirtualFree(buf, 0, MEM_RELEASE);
        len = 0;
        buf = nullptr;
    }

    size_t size() const { return len; }
    unsigned char* ptr() { return buf; }
    const unsigned char* ptr() const { return buf; }
};

例中 SecretBuf 是一个缓冲区类,其申请的内存会被锁定在物理内存中,不会与外存交换,可在一定程度上防止其他进程的恶意嗅探,保障缓冲区内数据的安全。SecretBuf 在构造函数中通过 VirtualLock 锁定物理内存,在析构函数中通过 VirtualUnlock 解除锁定,解锁之前有必要清除数据,否则解锁之后残留数据仍有可能被交换到外存,进一步可参见 ID_unsafeCleanup。

SecretBuf 的使用方法如下:

void foo() {
    SecretBuf buf(256);
    if (buf.ptr()) {
        ....             // Do something secret using buf.ptr()
    } else {
        ....             // Handle memory error
    }
}

在 Linux 等系统中可参见如下有相似功能的接口:

int mlock(const void* addr, size_t len);     // In <sys/mman.h>
int munlock(const void* addr, size_t len);
int mlockall(int flags);
int munlockall(void);


相关

ID_unsafeCleanup

参考

CWE-528
CWE-591
SEI CERT MEM06-C



▌R1.3 敏感数据在使用后应被有效清理

ID_unsafeCleanup       🛡 security warning


及时清理不再使用的敏感数据是重要的安全措施,且应保证清理过程不会因为编译器的优化而失效。

程序会反复利用内存,敏感数据可能会残留在未初始化的对象或对象之间的填充数据中,如果被存储到磁盘或传输到网络就会造成敏感信息的泄露,可参见 ID_secretLeak 和 ID_ignorePaddingData 的进一步讨论。

示例:

void foo() {
    char password[8] = {};
    ....
    memset(password, 0, sizeof(password));  // Non-compliant
}

示例代码调用 memset 覆盖敏感数据以达到清理目的,然而保存敏感信息的 password 为局部数组且 memset 之后没有再被引用,根据相关标准,编译器可将 memset 过程去掉,使敏感数据没有得到有效清理。C11 提供了 memset_s 函数以避免这种问题,某些平台和库也提供了相关支持,如 SecureZeroMemory、explicit_bzero、OPENSSL_cleanse 等不会被优化掉的函数。

在 C++ 代码中,可用 volatile 限定相关数据以避免编译器的优化,再用 std::fill_n 等方法清理,如:

void foo() {
    char password[8] = {};
    ....
    volatile char  v_padding = 0;
    volatile char* v_address = password;
    std::fill_n(v_address, sizeof(password), v_padding);  // Compliant
}


相关

ID_secretLeak
ID_ignorePaddingData

依据

ISO/IEC 9899:1999 5.1.2.3(3)
ISO/IEC 9899:2011 5.1.2.3(4)
ISO/IEC 9899:2011 K.3.7.4.1

参考

CWE-14
CWE-226
CWE-244
CWE-733
SEI CERT MSC06-C



▌R1.4 公共成员或全局对象不应记录敏感数据

ID_sensitiveName       🛡 security warning


公共成员、全局对象可被外部代码引用,如果存有敏感数据则可能会被误用或窃取。

示例:

extern string password;   // Non-compliant

struct A {
    string username;
    string password;      // Non-compliant
};

至少应将相关成员改为 private:

class A {
public:
    ....                  // Interfaces for accessing passwords safely
private:
    string username;
    string password;      // Compliant
};

敏感数据最好对引用者完全隐藏,避免被恶意分析、复制或序列化。使数据与接口进一步分离,可参见“Pimpl idiom”等模式。



参考

CWE-766



▌R1.5 与内存空间布局相关的信息不可被外界感知

ID_addressExposure       🛡 security warning


函数、对象、缓冲区的地址以及相关内存区域的长度等信息不可被外界感知,否则会成为攻击者的线索。

示例:

int foo(int* p, int n) {
    if (n >= some_value) {
        log("buffer address: %p, size: %d", p, n);   // Non-compliant
    }
}

示例代码将缓冲区的地址和长度输出到日志是不安全的,这种代码多以调试为目的,不应将其编译到产品的正式版本中。



相关

ID_bufferOverflow

参考

CWE-200



▌R1.6 与网络地址相关的信息不应写入代码

ID_hardcodedIP       🛡 security warning


在代码中记录网络地址不利于维护和移植,也容易暴露产品的网络结构,属于安全隐患。

示例:

string host = "10.16.25.93";    // Non-compliant
foo("172.16.10.36:8080");       // Non-compliant
bar("https://192.168.73.90");   // Non-compliant

应从配置文件中获取地址,并配以加密措施:

MyConf cfg;
string host = cfg.host();   // Compliant
foo(cfg.port());            // Compliant
bar(cfg.url());             // Compliant

特殊的 IP 地址可不受本规则限制,如:

0.0.0.0
255.255.255.255
127.0.0.1-127.255.255.255


相关

ID_addressExposure



▌R1.7 预判用户输入造成的不良后果

ID_hijack       🛡 security warning


须对用户输入的脚本、路径、资源请求等信息进行预判,对产生不良后果的输入予以拒绝。

示例:

Result foo() {
    return sqlQuery(
        "select * from db where key='%s'", userInput()   // Non-compliant
    );
}

设 userInput 返回用户输入的字符串,sqlQuery 将用户输入替换格式化占位符后执行 SQL 语句,如果用户输入“xxx’ or ‘x’='x”一类的字符串则相当于执行的是“select * from db where key=‘xxx’ or ‘x’=‘x’”,一个恒为真的条件使 where 限制失效,造成所有数据被返回,这是一种常见的攻击方式,称为“SQL 注入(SQL injection)”,对于 XPath、XQuery、LDAP 等脚本均需考虑这种问题,应在执行前判断用户输入的安全性。

又如:

string bar() {
    return readFile(
        "/myhome/mydata/" + userInput()   // Non-compliant
    );
}

这段代码意在将用户输入的路径限制在 /myhome/mydata 目录下,然而这么做是不安全的,如果用户输入带有“…/”这种相对路径,则仍可绕过限制,这也是一种常见的攻击方式,称为“路径遍历(directory traversal)”,应在读取文件之前判断路径的安全性。

注意,“用户输入”不单指人的手工输入,源自环境变量、配置文件以及其他软硬件的输入均在此范围内。



参考

CWE-23
CWE-73
CWE-89
CWE-943



▌R1.8 对资源设定合理的访问权限

ID_unlimitedAuthority       🛡 security warning


对资源设定合理的访问权限,避免为攻击者提供不应拥有的权限或能力。

权限的分类包括但不限于:

  • 文件、数据库等资源的读写权限
  • 计算、IO 过程的执行权限
  • 软硬件资源的占用权限

权限设定是产品设计与实现的重要环节,需落实相关的评审与测试。

示例:

#include <stdio.h>

int main() {
    umask(000);                     // Non-compliant
    FILE* fp = fopen("bar", "w");   // Old method
    ....
    fclose(fp);
}

例中 umask 函数开放了所有用户对文件的读写权限,这是很不安全的,进程之间不应直接通过文件通信,应实现安全的接口和交互机制。

由于历史原因,C 语言的 fopen 和 C++ 语言的 fstream 都不能确保文件只能被当前用户访问,C11 提供了 fopen_s,C++17 提供了 std::filesystem::permissions 以填补这方面的需求。

C11 fopen_s 简例:

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>

int main() {
    FILE* fp = NULL;
    errno_t e = fopen_s(&fp, "bar", "w");   // Good
    ....
    fclose(fp);
}

与 fopen 不同,fopen_s 可以不受 umask 等函数的影响,直接将文件的权限设为当前用户私有,其他用户不可访问,降低了文件被窃取或篡改的风险,是一种更安全的方法。

除此之外,如果需要对资源进行更精细的权限管理,可参见“access control list(ACL)”。



依据

ISO/IEC 9899:2011 K.3.5.2.1(7)
ISO/IEC 14882:2017 30.10.15.26

参考

CWE-266
CWE-732
SEI CERT FIO06-C



▌R1.9 对用户落实有效的权限管理

ID_improperAuthorization       🛡 security warning


需落实有效的权限管理,相关措施包括但不限于:

  • 落实授权与认证机制,提供多因素认证
  • 遵循最小特权原则,对资源和相关算法设置合理的访问或执行权限
  • 避免仅在客户端认证而非服务端认证
  • 检查请求是否符合用户的权限设定,拒绝无权限的请求
  • 用户放弃某项权限后,应确保相关权限不再生效
  • 遵循合理的“认证 - 执行”顺序,避免复杂度攻击或早期放大攻击
  • 保证信道完整性,对相关用户进行充分的身份认证,避免中间人攻击
  • 验证通信通道的源和目的地,拒绝非预期的请求和应答
  • 避免攻击者使用重放攻击等手段绕过身份认证或干扰正常运营
  • 避免不恰当地信任反向 DNS(关注 DNS Cache Poisoning)
  • 避免过于严格且易触发的账户锁定机制,使攻击者通过锁定账户干扰正常运营

权限管理与安全直接相关,应落实严格的评审、测试以及攻防演练。

示例:

Result foo() {
    auto req = getRequest();
    auto res = sqlQuery(
        "select * from db where key='%s'", req["key"]   // Non-compliant
    );
    return res;
}

设例中 req 对应用户请求,sqlQuery 将请求中的 key 字段替换格式化占位符后执行查询,这个模式存在多种问题,应先判断用户是否具有读取数据库相关字段的权限,而且还应判断 req[“key”] 的值是否安全,详见 ID_hijack。

又如:

void bar(User* user) {
    auto buf = read_large_file();
    if (is_admin(user)) {           // Non-compliant
        do_something(buf);
    }
}

设例中 read_large_file 读取大型文件,is_admin 进行身份认证,在身份认证之前访问资源使得攻击者不必获取有效账号即可消耗系统资源,从而对系统造成干扰,所以应该在访问资源之前进行身份认证。



参考

CWE-285
CWE-350



▌R1.10 避免引用危险符号名称

ID_dangerousName       🛡 security warning


弱加密、弱哈希、弱随机、不安全的协议等相关库、函数、类、宏、常量等名称不应出现在代码中。

这种危险符号名称主要来自:

  • 低质量随机数生成算法,如 srand、rand 等
  • 不再适用的哈希算法,如 MD2、MD4、MD5、MD6、RIPEMD 以及 SHA-1 等
  • 非加密协议,如 HTTP、FTP 等
  • 低版本的传输层安全协议,如 TLSv1.2 之前的版本
  • 弱加密算法,如 DES、3DES 等

示例:

#include <openssl/md5.h>   // Non-compliant, obsolete hash algorithm

const string myUrl = "http://foo/bar";   // Non-compliant, use https instead

void foo() {
    MD5_CTX c;       // Non-compliant
    MD5_Init(&c);    // Non-compliant, obsolete hash algorithm
    ....
}

void bar() {
    srand(0);        // Non-compliant, unsafe random seed
    EVP_des_ecb();   // Non-compliant, unsafe encryption algorithm
    ....
}


参考

CWE-326
CWE-327
SEI CERT MSC25-C



▌R1.11 避免使用危险接口

ID_dangerousFunction       🛡 security warning


由于历史原因,有些系统接口甚至标准库函数存在缺陷,无法安全使用,也有一些接口的使用条件很苛刻,难以安全使用。

示例:

gets       // The most dangerous function
mktemp     // Every use of ‘mktemp’ is a security risk, use ‘mkstemp’ instead
getpass    // Unsafe and not portable
crypt      // Unsafe, exhaustive searches of the key space are possible
getpw      // It may overflow the provided buffer, use ‘getpwuid’ instead
cuserid    // Not portable and unreliable, use ‘getpwuid(geteuid())’ instead
chgrp      // Prone to TOCTOU race conditions, use ‘fchgrp’ instead
chown      // Prone to TOCTOU race conditions, use ‘fchown’ instead
chmod      // Prone to TOCTOU race conditions, use ‘fchmod’ instead

SuspendThread       // Forced suspension of a thread can cause many problems
TerminateThread     // Forced termination of a thread can cause many problems
GlobalMemoryStatus        // Return incorrect information, use ‘GlobalMemoryStatusEx’ instead
SetProcessWorkingSetSize  // Cause adverse effects on other processes and the entire system

例中 gets 函数不检查缓冲区边界,无法安全使用;TerminateThread 等 Windows API 强制终止线程,线程持有的资源难以正确释放,极易导致泄漏或死锁等问题,应避免使用这类函数。



参考

CWE-242
CWE-676



▌R1.12 避免使用已过时的接口

ID_obsoleteFunction       🛡 security warning


避免使用在相关标准中已过时的接口,应改用更完善的替代方法以规避风险,提高可移植性。

对于过时的 C++ 标准库接口,本规则特化为 ID_obsoleteStdFunction。

示例:

asctime         // Use ‘strftime’ instead
bcmp            // Use ‘memcmp’ instead
bcopy           // Use ‘memmove’ or ‘memcpy’ instead
bsd_signal      // Use ‘sigaction’ instead
bzero           // Use ‘memset’ instead
ctime           // Use ‘strftime’ instead
gethostbyaddr   // Use ‘getnameinfo’ instead
gethostbyname   // Use ‘getaddrinfo’ instead
getwd           // Use ‘getcwd’ instead
mktemp          // Use ‘mkstemp’ instead
usleep          // Use ‘nanosleep’ instead
utime           // Use ‘utimensat’ instead
vfork           // Use ‘fork’ instead
wcswcs          // Use ‘wcsstr’ instead

pthread_attr_getstackaddr   // Use ‘pthread_attr_getstack’ instead
pthread_attr_setstackaddr   // Use ‘pthread_attr_setstack’ instead

CreateToolbarEx      // Use ‘CreateWindowEx’ instead
InitCommonControls   // Use ‘InitCommonControlsEx’ instead
NtQuerySystemTime    // Use ‘GetSystemTimeAsFileTime’ instead
RegCreateKey         // Use ‘RegCreateKeyEx’ instead
WinExec              // Use ‘CreateProcess’ instead

例中 C89 引入的 ctime、asctime 等函数在 POSIX.1-2008 标准中已过时,应改用 strftime 函数;RegCreateKey 等 16 位 Windows API 在 32 和 64 位平台中不应再被使用。



相关

ID_obsoleteStdFunction

参考

CWE-477
SEI CERT MSC24-C



▌R1.13 避免除 0 等计算异常

ID_divideByZero       🛡 security error


除 0 等在数学上没有定义的运算、浮点异常、非法指令、段错误等问题称为“计算异常”,意味着严重的底层运行时错误,而且这种异常无法用语言层面的常规方法捕获。

示例:

int foo(int n) {
    if (n) {
        return 100 / n;   // Compliant
    }
    return 200 / n;   // Non-compliant, undefined behavior
}

整数除 0 往往会使程序崩溃,浮点数除 0 可以产生“Inf”或“NaN”等无效结果,在某些环境中也可以设置浮点异常使程序收到特定信号。

崩溃会使程序异常终止,无法或难以执行必要的善后工作。如果崩溃可由外部输入引起,会被攻击者利用从而迫使程序无法正常工作,具有高可靠性要求的服务类程序更应该注意这一点,可参见“拒绝服务攻击”。对于客户端程序,也要防止攻击者对崩溃产生的“core dump”进行恶意调试,避免泄露敏感数据,总之程序的健壮性与安全性是紧密相关的。



相关

ID_sig_illReturn

依据

ISO/IEC 9899:1999 6.5.5(5)-undefined
ISO/IEC 9899:2011 6.5.5(5)-undefined
ISO/IEC 14882:2003 5.6(4)-undefined
ISO/IEC 14882:2011 5.6(4)-undefined
ISO/IEC 14882:2017 8.6(4)-undefined

参考

CWE-189
CWE-369
C++ Core Guidelines ES.105



▌R1.14 选择安全的异常处理方式

ID_deprecatedErrno       🛡 security warning


避免使用 errno 和与其相同的模式,应根据实际需求选择通过函数返回值或 C++ 异常机制来处理异常情况。

errno 被设定的位置和被读取的位置相距较远,不遵循固定的静态结构,极易误用,是不安全的异常处理方式,对异常情况的错误处理往往会成为业务漏洞,使攻击者轻易地实现其目的。

示例:

void foo() {
    if (somecall() == FAILED) {
        printf("somecall failed\n");
        if (errno == SOME_VALUE) {     // Non-compliant
            ....
        }
    }
}

例中 somecall 执行异常,通过 errno 获取异常信息,但 errno 的值会被 printf 修改,相应的异常处理也失去了意义。

又如:

void bar(const char* s) {
    int i = atoi(s);
    if (errno) {        // Non-compliant
        ....
    }
}

errno 并不能反映所有异常情况,atoi 等函数与 errno 无关,例中 errno 的值来自函数外部难以预料的位置,相应的异常处理也将是错误的。



参考

C++ Core Guidelines E.28
MISRA C 2004 20.5
MISRA C++ 2008 19-3-1



▌R1.15 不应产生或依赖未定义的行为

ID_undefinedBehavior       🛡 security warning


未定义的行为(undefined behavior)是指程序在语言标准中没有定义的行为,一般由错误的代码实现引起,可能是崩溃,也可能没有实质危害,这种行为的结果是不可预期的,不应使程序产生或依赖未定义的行为。

对未定义行为的介绍和约束是本规则集合的重要内容,将在后续章节中深入讨论,在附录中也有详细介绍。

示例:

int foo(int i) {
    if (i + 1 <= i)   // Determine overflow, but it’s undefined
        ....          // Handle overflow, may be invalid
    else
        return i + 1;
}

示例代码用 i + 1 <= i 判断是否溢出,但有符号整数溢出的后果是未定义的,这种判断可能是无效的,甚至某些编译器会认为 i + 1 <= i 恒为假而免去 if 分枝的内容,直接返回 i + 1。

应改为:

int foo(int i) {
    if (i != INT_MAX)   // Well defined
        return i + 1;
    else
        ....            // Handle overflow
}


相关

ID_unspecifiedBehavior
ID_implementationDefinedFunction

依据

ISO/IEC 9899:2011 3.4.3
ISO/IEC 9899:2011 J.2
ISO/IEC 14882:2011 1.3.24

参考

CWE-758
SEI CERT MSC15-C



▌R1.16 不应依赖未声明的行为

ID_unspecifiedBehavior       🛡 security warning


语言标准允许程序的某些行为可由编译器自行定义,且无需提供文档说明,这种行为称为未声明的行为(unspecified behavior),具有不确定性,也会导致可移植性问题,故不应使程序依赖未声明的行为。

对未声明行为的介绍和约束是本规则集合的重要内容,将在后续章节中深入讨论。

示例:

const char* p = "ABC";
const char* q = "ABC";

assert(p == q);   // Unspecified behavior

相同字符串常量的地址是否相同是未声明的,例中的断言可能会失效,而且要注意,未声明的行为即使在同一编译器的不同版本之间也可能会有差异。



相关

ID_undefinedBehavior
ID_implementationDefinedFunction

依据

ISO/IEC 9899:2011 3.4.4
ISO/IEC 9899:2011 J.1
ISO/IEC 14882:2011 1.3.25

参考

CWE-758



▌R1.17 避免依赖由实现定义的行为

ID_implementationDefinedFunction       🛡 security warning


语言标准允许程序的某些行为可由编译器自行定义,这种行为称为由实现定义的行为(implementation-defined behavior),虽然有文档支持,但也会增加移植或兼容等方面的成本。

示例:

  • cstdlib、stdlib.h 中的 abort、exit、_Exit、quick_exit、getenv、system 等函数
  • ctime、time.h 中的 clock 等函数
  • csignal、signal.h 中的 signal 等函数

这些函数的行为取决于编译器、库或环境的生产厂家,同一个函数不同的厂家会有不同的实现,故称这种函数的行为是“由实现定义”的。有高可靠性要求的软件系统应避免使用这种函数,否则需明确各种实现上的具体差异,增加了移植、发布以及兼容性等多方面的成本。

#include <cstdlib>

void foo() {
    abort();   // Non-compliant
}

调用 abort 函数会终止进程,但打开的流是否会被关闭,缓冲区内的数据是否会写入文件,临时文件是否会被清理则由实现定义。



相关

ID_undefinedBehavior
ID_unspecifiedBehavior

依据

ISO/IEC 9899:2011 7.14.1.1(3)-implementation
ISO/IEC 9899:2011 7.22.4.1(2)-implementation
ISO/IEC 9899:2011 7.22.4.4(5)-implementation
ISO/IEC 9899:2011 7.22.4.5(2)-implementation
ISO/IEC 9899:2011 7.22.4.6(2)-implementation
ISO/IEC 9899:2011 7.22.4.7(4)-implementation
ISO/IEC 9899:2011 7.22.4.8(3)-implementation
ISO/IEC 9899:2011 7.27.2.1(3)-implementation

参考

CWE-474
CWE-589
MISRA C 2004 20.8
MISRA C 2004 20.11
MISRA C 2004 20.12
MISRA C 2012 21.5
MISRA C 2012 21.8
MISRA C 2012 21.10
MISRA C++ 2008 18-0-3
MISRA C++ 2008 18-0-4
MISRA C++ 2008 18-7-1



▌R1.18 保证组件的可靠性

ID_untrustedComponent       🛡 security suggestion


导入库、配置、数据等组件时应判断其可靠性,对不受信任的组件予以拒绝。

示例:
利用数字签名判断 DLL 等动态库的可靠性,代码可参考“WinVerifyTrust”等 API 的使用。



相关

ID_untrustedThirdParty

参考

CWE-1357



▌R1.19 保证第三方软件的可靠性

ID_untrustedThirdParty       🛡 security suggestion


应检查引入的第三方代码是否可靠,避免由第三方软件引入安全风险。

示例:
利用“软件组成分析(SCA)”工具检查第三方库的安全性。



相关

ID_untrustedComponent

参考

CWE-1395



▌R1.20 隔离非正式功能的代码

ID_backDoor       🛡 security warning


非正式功能的代码,如用于调试、测试的代码或历史遗留代码,在产品的发布版本中不应生效,否则会导致泄露信息或打破正常流程等非预期的结果。

示例:

User u = get_user_input();
if (authenticate(u) || u.name() == "debug")   // Non-compliant, back door
{
    ....   // Login successful
}

示例代码进行了用户身份验证,但直接放过 debug 这种特殊用户名是不符合要求的。



参考

CWE-215
CWE-489
CWE-1295



▌R1.21 启用平台和编译器提供的防御机制

ID_missingHardening       🛡 security suggestion


针对一些常见攻击,平台和编译器会提供防御机制,如:

程序应利用这种机制加强自身的安全性,进一步可参见“[security hardening](https://en.wikipedia.org/wiki/Hardening_(computing “security hardening”))”。

示例:

// In test.c
#include <stdio.h>

int main(void) {
    printf("%p\n", main);
}

如果在 Linux 等平台上按如下方式编译:

cc test.c -o test

各函数的地址在虚拟内存中是固定的,易被攻击者猜中,进而施展攻击手段。

当平台启用了“ASLR”机制,再按如下方式编译:

cc test.c -o test -fPIE -pie

可使程序各结构的地址随机化,函数的地址在每次运行时均不相同,有效提高了攻击难度。

如无特殊原因,在编译程序时不应屏蔽这种防御机制,如:

cc test.c -o test -z execstack           # Non-compliant, disable NX
cc test.c -o test -z norelro             # Non-compliant, disable RELRO
cc test.c -o test -fno-stack-protector   # Non-compliant, disable CANARY

如果必须屏蔽,应落实相关的评审与测试。



相关

ID_unsafeCompileOption



▌R1.22 禁用不安全的编译选项

ID_unsafeCompileOption       ⛔️ security warning


掩盖错误、不符合标准、屏蔽安全措施等不安全的编译选项应被禁用。

示例:

c++ test.cpp -o test -fpermissive -w       # Non-compliant
c++ test.cpp -o test -fno-access-control   # Non-compliant
c++ test.cpp -o test -ffast-math           # Non-compliant

例中选项 -fpermissive 会使一些编译错误降为警告,-w 会隐藏警告,-fno-access-control 会打破语言规则,使类成员不再受 private、protected 等关键字限制,-ffast-math 虽然会提高程序的运算效率,但不再遵守相关 IEEE 或 ISO 标准,这种编译选项均不应使用。



配置

forbiddenOptions:应被禁用的编译选项

相关

ID_missingHardening



2. Resource

▌R2.1 不可失去对已分配资源的控制

ID_resourceLeak       :drop_of_blood: resource warning


对于动态分配的资源,其地址、句柄或描述符等标志性信息不可被遗失,否则资源无法被访问也无法被回收,这种问题称为“资源泄漏”,会导致资源耗尽或死锁等问题,使程序无法正常运行。

在资源被回收之前,记录其标志性信息的变量如果:

  • 均被重新赋值
  • 生命周期均已结束
  • 所在线程均被终止

相关资源便失去了控制,无法再通过正常手段访问相关资源。

示例:

int fd;
fd = open("a", O_RDONLY);   // Open a file descriptor
read(fd, buf1, 100);
fd = open("b", O_RDONLY);   // Non-compliant, the previous descriptor is lost
read(fd, buf2, 100);

例中变量 fd 记录文件资源描述符,在回收资源之前对其重新赋值会导致资源泄漏。



相关

ID_memoryLeak
ID_asynchronousTermination

参考

CWE-772
C++ Core Guidelines P.8



▌R2.2 不可失去对已分配内存的控制

ID_memoryLeak       :drop_of_blood: resource warning


动态分配的内存地址不可被遗失,否则相关内存无法被访问也无法被回收,这种问题称为“内存泄漏(memory leak)”,会导致可用内存被耗尽,使程序无法正常运行。

程序需要保证内存分配与回收之间的流程可达,且不可被异常中断,相关线程也不可在中途停止。

本规则是 ID_resourceLeak 的特化。

示例:

void foo(size_t n) {
    void* p = malloc(n);
    if (cond) {
        return;  // Non-compliant, ‘p’ is lost
    }
    ....
    free(p);
}

例中局部变量 p 记录已分配的内存地址,释放前在某种情况下函数返回,之后便再也无法访问到这块内存了,导致内存泄露。

又如:

void bar(size_t n) {
    void* p = malloc(n);
    if (n < 100) {
        p = realloc(p, 100);  // Non-compliant, ‘p’ may be lost
    }
    ....
}

当 realloc 函数分配失败时会返回空指针,p 指向的原内存空间不会被释放,但 p 被赋值为空,导致内存泄露,这是一种常见错误。



相关

ID_resourceLeak
ID_ownerlessResource
ID_throwInConstructor
ID_memberDeallocation
ID_multiAllocation

依据

ISO/IEC 9899:1999 7.20.3(1)
ISO/IEC 9899:2011 7.22.3(1)
ISO/IEC 14882:2003 3.7.3.1(2)
ISO/IEC 14882:2003 3.7.4.1(2)

参考

CWE-401
C++ Core Guidelines P.8
C++ Core Guidelines E.13



▌R2.3 不可访问未初始化或已释放的资源

ID_illAccess       :drop_of_blood: resource error


访问未初始化或已释放的资源属于逻辑错误,会导致标准未定义的行为。

对于访问未初始化的局部对象,本规则特化为 ID_localInitialization;对于解引用未初始化或已被释放的指针,本规则特化为 ID_wildPtrDeref 、ID_danglingDeref。

示例:

void foo(const char* path, char buf[], size_t n) {
    FILE* f;
    if (path != NULL) {
        f = fopen(path, "rb");
    }
    fread(buf, 1, n, f);   // Non-compliant, ‘f’ may be invalid
    fclose(f);
}

void bar(FILE* f, char buf[], size_t n) {
    if (feof(f)) {
        fclose(f);
    }
    fread(buf, 1, n, f);   // Non-compliant, ‘f’ may be closed
}


相关

ID_wildPtrDeref
ID_danglingDeref
ID_localInitialization

依据

ISO/IEC 9899:1999 7.19.3(4)
ISO/IEC 9899:2011 7.21.3(4)

参考

CWE-672
CWE-908
SEI CERT FIO46-C
SEI CERT MEM30-C
SEI CERT MEM50-CPP
SEI CERT EXP33-C
SEI CERT EXP53-CPP



▌R2.4 使资源接受对象化管理

ID_ownerlessResource       :drop_of_blood: resource warning


将资源托管于类的对象,使资源的生命周期协同于对象的生命周期,避免分散处理分配与回收等问题,是 C++ 程序设计中的重要方法。

动态申请的资源如果只能通过普通指针或变量访问,不受对象的构造和析构等机制控制,则称为“无主”资源,极易产生泄漏或死锁等问题。应尽量使用标准库提供的容器、智能指针以及资源对应的类,避免直接使用 new、delete 以及底层资源管理接口。

示例:

int* p = new int[8];   // Non-compliant, ownerless
....                   // If any exception is thrown,
                       // or any wrong jump occurs, the memory leaks

struct X { int* p; };
X x;
x.p = new int[8];      // Non-compliant, no destructor, ‘x’ is not an owner
....

delete[] p;     // Non-compliant, explicit delete
delete[] x.p;   // Non-compliant

例中用不受析构函数控制的指针保存 new 表达式的结果,以及对应的 delete 表达式均不符合要求。

应将资源托管于类的对象:

class Mgr {
    int* p;
public:
    Mgr(size_t n): p(new int[n]) {}
   ~Mgr() { delete[] p; }
};

Mgr m(8);   // Compliant, ‘m’ is the owner of the resource

例中 m 对象负责资源的分配与回收,称 m 对象拥有资源的所有权,相关资源的生命周期与对象的生命周期一致,有效避免了资源泄漏或错误回收等问题。针对成员的 new、delete 可不受本规则限制,但应优先使用容器或智能指针。

资源的所有权可以发生转移,但应保证转移前后均有对象负责管理资源,并且在转移过程中不会产生异常。进一步理解对象化管理方法,可参见“RAII(Resource Acquisition Is Initialization)”等机制。

另外,底层资源管理接口也不应直接在业务代码中使用,如:

void foo(const TCHAR* path) {
    WIN32_FIND_DATA ffd;
    HANDLE h = FindFirstFile(path, &ffd);  // Non-compliant, ownerless
    ....
    CloseHandle(h);  // Is it right?
}

例中 FindFirstFile 是 Windows API,返回的资源句柄对应“无主”资源,需要显式回收。

应对其合理封装:

class MY_FIND_DATA
{
    struct HANDLE_DELETER
    {
        using pointer = HANDLE;
        void operator()(pointer p) { FindClose(p); }
    };
    WIN32_FIND_DATA ffd;
    unique_ptr<HANDLE, HANDLE_DELETER> uptr;

public:
    MY_FIND_DATA(const TCHAR* path): uptr(FindFirstFile(path, &ffd)) {}
    ....
    HANDLE handle() { return uptr.get(); }
};

将 FindFirstFile 及其相关数据封装成一个类,由 unique_ptr 对象保存 FindFirstFile 的结果,FindClose 是资源的回收方法,将其作为 unique_ptr 对象的组成部分,使资源可以被自动回收。



参考

C++ Core Guidelines R.11
C++ Core Guidelines R.12



▌R2.5 资源的分配与回收方法应成对提供

ID_incompleteNewDeletePair       :drop_of_blood: resource suggestion


资源的分配和回收方法应在同一库或主程序等可执行模块、类等逻辑模块中提供。

如果一个模块分配的资源需要另一个模块回收,会打破模块之间的独立性,增加维护成本,而且 so、dll、exe 等可执行模块一般都有独立的堆栈,跨模块的分配与回收往往会造成严重错误。

示例:

// In a.dll
int* foo() {
    return (int*)malloc(1024);
}

// In b.dll
void bar() {
    int* p = foo();
    ....
    free(p);   // Non-compliant, crash
}

例中 a.dll 分配的内存由 b.dll 释放,相当于混淆了不同堆栈中的数据,程序一般会崩溃。

应改为:

// In a.dll
int* foo_alloc() {
    return (int*)malloc(1024);
}

void foo_dealloc(int* p) {
    free(p);
}

// In b.dll
void bar() {
    int* p = foo_alloc();
    ....
    foo_dealloc(p);   // Compliant
}

修正后 a.dll 成对提供分配回收函数,b.dll 配套使用这些函数,避免了冲突。

类等逻辑模块提供了分配方法,也应提供回收方法,如重载了 new 运算符,也应重载相应的 delete 运算符:

class A {
    void* operator new(size_t);   // Non-compliant, missing ‘operator delete’
};

class B {
    void operator delete(void*);   // Non-compliant, missing ‘operator new’
};

class C {
    void* operator new(size_t);   // Compliant
    void operator delete(void*);   // Compliant
};


相关

ID_memberDeallocation
ID_crossModuleTransfer
ID_incompatibleDealloc

参考

C++ Core Guidelines R.15
SEI CERT DCL54-CPP



▌R2.6 资源的分配与回收方法应配套使用

ID_incompatibleDealloc       :drop_of_blood: resource error


使用了某种分配方法,就应使用与其配套的回收方法,否则会引发严重错误。

示例:

void foo() {
    T* p = new T;
    ....
    free(p);   // Non-compliant, use ‘delete’ instead
}

void bar(size_t n) {
    char* p = (char*)malloc(n);
    ....
    delete[] p;   // Non-compliant, use ‘free’ instead
}

不同的分配回收方法属于不同的资源管理体系,用 new 分配的资源应使用 delete 回收,malloc 分配的应使用 free 回收。



相关

ID_incompleteNewDeletePair

依据

ISO/IEC 9899:1999 7.20.3.2(2)-undefined
ISO/IEC 9899:2011 7.22.3.3(2)-undefined
ISO/IEC 14882:2003 3.7.3.2(3)
ISO/IEC 14882:2011 3.7.4.2(3)-undefined

参考

SEI CERT MEM51-CPP



▌R2.7 不应在模块之间传递容器类对象

ID_crossModuleTransfer       :drop_of_blood: resource warning


在库或主程序等可执行模块之间传递容器类对象会造成分配回收方面的冲突。

与资源管理相关的对象,如流、字符串、智能指针以及自定义对象均不应在模块间传递。

不同的可执行模块往往具有独立的资源管理机制,跨模块的分配与回收会造成严重错误,而且不同的模块可能由不同的编译器生成,对同一对象的实现也可能存在冲突。

示例:

// In a.dll
void foo(vector<int>& v) {
    v.reserve(100);
}

// In b.exe
int main() {
    vector<int> v {   // Allocation in b.exe
        1, 2, 3
    };
    foo(v);   // Non-compliant, reallocation in a.dll, crash
}

例中容器 v 的初始内存由 b.exe 分配,b.exe 与 a.dll 具有独立的堆栈,由于模板库的内联实现,reserve 函数会调用 a.dll 的内存管理函数重新分配 b.exe 中的内存,造成严重错误。



相关

ID_incompleteNewDeletePair
ID_ABIConflict



▌R2.8 不应在模块之间传递非标准布局类型的对象

ID_ABIConflict       :drop_of_blood: resource warning


非标准布局类型的运行时特性依赖编译器的具体实现,在不同编译器生成的模块间传递这种类型的对象会导致运行时错误。

标准布局(standard-layout)”类型的主要特点:

  • 没有虚函数也没有虚基类
  • 所有非静态数据成员均具有相同的访问权限
  • 所有非静态数据成员和位域都在同一个类中声明
  • 不存在相同类型的基类对象
  • 没有非标准布局的基类
  • 没有非标准布局和引用类型的非静态数据成员

除非模块均由同一编译器的同一版本生成,否则不具备上述特点的对象不应在模块之间传递。

示例:

// a.dll
class A {
    ....
public:
    virtual void foo();   // Non standard-layout
};

void bar(A&);

// b.exe
int main() {
    A a;
    bar(a);   // Non-compliant
}

设例中 a.dll 和 b.exe 由不同的编译器生成,b.exe 中定义的 a 对象被传递给了 a.dll 中定义的接口,由于存在虚函数,不同的编译器对 a 对象的内存布局会有不同的解读,从而造成冲突。



依据

ISO/IEC 14882:2011 9(7)
ISO/IEC 14882:2017 12(7)

参考

SEI CERT EXP60-CPP



▌R2.9 对象申请的资源应在析构函数中释放

ID_memberDeallocation       :drop_of_blood: resource warning


对象在析构函数中释放自己申请的资源是 C++ 程序设计的重要原则,不可被遗忘,也不应要求用户释放。

示例:

class A {
    int* p = nullptr;

public:
    A(size_t n): p(new int[n]) {
    }

   ~A() {  // Non-compliant, must delete[] p
    }
};

例中成员 p 与内存分配有关,但析构函数为空,不符合本规则要求。



相关

ID_memoryLeak
ID_resourceLeak

参考

C++ Core Guidelines C.31
C++ Core Guidelines E.6



▌R2.10 对象被移动后应重置状态再使用

ID_useAfterMove       :drop_of_blood: resource warning


对象被移动后在逻辑上不再有效,如果没有通过清空数据或重新初始化等方法更新对象的状态,不应再使用该对象。

示例:

#include <vector>

using V = std::vector<int>;

void foo(V& a, V& b)
{
    a = std::move(b);   // After moving, the state of ‘b’ is unspecified
    b.push_back(0);     // Non-compliant
}

例中容器 b 的数据被移动到容器 a,可能是通过交换的方法实现的,也可能是通过其他方法实现的,标准容器被移动后的状态在 C++ 标准中是未声明的,程序不应依赖未声明的状态。

应改为:

void foo(V& a, V& b)
{
    a = std::move(b);
    b.clear();          // Clear
    b.push_back(0);     // Compliant
}


相关

ID_unsuitableMove

依据

ISO/IEC 14882:2011 17.6.5.15(1)-unspecified
ISO/IEC 14882:2017 20.5.5.15(1)-unspecified

参考

C++ Core Guidelines ES.56
SEI CERT EXP63-CPP



▌R2.11 构造函数抛出异常需避免相关资源泄漏

ID_throwInConstructor       :drop_of_blood: resource warning


构造函数抛出异常表示对象构造失败,不会再执行相关析构函数,需要保证已分配的资源被有效回收。

示例:

class A {
    int *a, *b;

public:
    A(size_t n):
        a(new int[n]),
        b(new int[n])     // The allocations may fail
    {
        if (sth_wrong) {
            throw E();    // User exceptions
        }
    }

   ~A() {                 // May be invalid
        delete[] a;
        delete[] b;
    }
};

例中内存分配可能会失败,抛出 bad_alloc 异常,在某种条件下还会抛出自定义的异常,任何一种异常被抛出析构函数就不会被执行,已分配的资源就无法被回收,但已构造完毕的对象还是会正常析构的,所以应采用对象化资源管理方法,使资源可以被自动回收。

可改为:

A::A(size_t n) {
    // Use objects to hold resources
    auto holder_a = make_unique<int[]>(n);
    auto holder_b = make_unique<int[]>(n);

    // Do the tasks that may throw exceptions
    if (sth_wrong) {
        throw E();
    }

    // Transfer ownership, make sure no exception is thrown
    a = holder_a.release();
    b = holder_b.release();
}

先用 unique_ptr 对象持有资源,完成可能抛出异常的事务之后,再将资源转移给相关成员,转移的过程不可抛出异常,这种模式可以保证异常安全,如果有异常抛出,资源均可被正常回收。对遵循 C++11 及之后标准的代码,建议用 make_unique 函数代替 new 运算符。

示例代码意在讨论一种通用模式,实际代码可采用更直接的方式:

class A {
    vector<int> a, b;  // Or use ‘unique_ptr’

public:
    A(size_t n): a(n), b(n) {  // Safe and brief
        ....
    }
};

保证已分配的资源时刻有对象负责回收是重要的设计原则,可参见 ID_ownerlessResource 的进一步讨论。

注意,“未成功初始化的对象”在 C++ 语言中是不存在的,应避免相关逻辑错误,如:

struct T {
    A() { throw CtorException(); }
};

void foo() {
    T* p = nullptr;
    try {
        p = new T;
    }
    catch (CtorException&) {
        delete p;              // Logic error, ‘p’ is nullptr
        return;
    }
    ....
    delete p;
}

例中 T 类型的对象在构造时抛出异常,而实际上 p 并不会指向一个未能成功初始化的对象,赋值被异常中断,catch 中的 p 仍然是一个空指针,new 表达式中抛出异常会自动回收已分配的内存。



相关

ID_ownerlessResource
ID_multiAllocation
ID_memoryLeak

依据

ISO/IEC 14882:2003 5.3.4(17)
ISO/IEC 14882:2011 5.3.4(18)
ISO/IEC 14882:2017 8.3.4(21)



▌R2.12 不可重复释放资源

ID_doubleFree       :drop_of_blood: resource error


重复释放资源会导致标准未定义的行为。

由于多种原因,资源管理系统难以甚至无法预先判断资源是否已被回收,一旦重复释放资源,可能会直接破坏资源管理系统的数据结构,导致不可预期的错误。

示例:

void foo(const char* path) {
    FILE* p = fopen(path, "r");
    if (p) {
        ....
        fclose(p);
    }
    fclose(p);  // Non-compliant, closed twice, undefined behavior
}


相关

ID_missingResetNull

依据

ISO/IEC 9899:1999 7.20.3.2(2)-undefined
ISO/IEC 9899:2011 7.22.3.3(2)-undefined
ISO/IEC 14882:2003 3.7.3.2(4)-undefined
ISO/IEC 14882:2011 3.7.4.2(4)-undefined

参考

CWE-415
SEI CERT MEM00-C



▌R2.13 用 delete 释放对象需保证其类型完整

ID_deleteIncompleteType       :drop_of_blood: resource warning


如果用 delete 释放“不完整类型(incomplete type)”的对象,且对象的完整类型具有 non-trivial 析构函数,会导致标准未定义的行为。

示例:

struct T;   // Forward declaration, the type is incomplete

void foo(T* p) {
    delete p;      // Non-compliant, undefined behavior
}

struct T {
   ~T();     // Non-trivial destructor
};

例中指针 p 被释放时,其类型是不完整的,如果指针的完整类型以及相关基类或非静态成员具有显式定义的非默认析构函数,即 non-trivial 析构函数,会导致未定义的行为,相关析构函数可能不会正确执行。

应保证指针的类型在释放前具有完整声明:

struct T {
   ~T();
};

void foo(T* p) {
    delete p;      // Compliant
}


依据

ISO/IEC 14882:2003 5.3.5(5)-undefined
ISO/IEC 14882:2011 5.3.5(5)-undefined



▌R2.14 用 delete 释放对象不可多写中括号

ID_excessiveDelete       :drop_of_blood: resource error


用 new 分配的对象应该用 delete 释放,不可用 delete[] 释放,否则导致标准未定义的行为。

示例:

auto* p = new X;  // One object
....
delete[] p;       // Non-compliant, use ‘delete p;’ instead


相关

ID_insufficientDelete

依据

ISO/IEC 14882:2003 5.3.5(2)-undefined
ISO/IEC 14882:2011 5.3.5(2)-undefined
ISO/IEC 14882:2017 8.3.5(2)-undefined

参考

C++ Core Guidelines ES.61



▌R2.15 用 delete 释放数组不可漏写中括号

ID_insufficientDelete       :drop_of_blood: resource error


用 new[] 分配的数组应该用 delete[] 释放,不可漏写中括号,否则导致标准未定义的行为。

示例:

void foo(int n) {
    auto* p = new X[n];  // n default-constructed X objects
    ....
    delete p;            // Non-compliant, use ‘delete[] p;’ instead
}

在某些环境中,可能只有第一个对象的析构函数被执行,其他对象的析构函数都没有被执行,如果对象与资源分配有关就会导致资源泄漏。



相关

ID_excessiveDelete

依据

ISO/IEC 14882:2003 5.3.5(2)-undefined
ISO/IEC 14882:2011 5.3.5(2)-undefined
ISO/IEC 14882:2017 8.3.5(2)-undefined

参考

C++ Core Guidelines ES.61



▌R2.16 不可释放非动态分配的内存

ID_illDealloc       :drop_of_blood: resource error


释放非动态分配的内存会导致标准未定义的行为。

本规则是 ID_incompatibleDealloc 的特化。

示例:

void bar() {
    int i;
    ....
    free(&i);   // Non-compliant, undefined behavior
}

在栈上分配的内存空间不需要显式回收,否则会导致严重的运行时错误。



相关

ID_incompatibleDealloc

依据

ISO/IEC 9899:1999 7.20.3.2(2)-undefined
ISO/IEC 9899:2011 7.22.3.3(2)-undefined
ISO/IEC 14882:2003 5.3.5(2)-undefined
ISO/IEC 14882:2011 5.3.5(2)-undefined

参考

MISRA C 2012 22.2
SEI CERT MEM34-C



▌R2.17 在一个表达式语句中最多使用一次 new

ID_multiAllocation       :drop_of_blood: resource warning


由于子表达式的求值顺序存在很多未声明的情况,在表达式中多次显式分配资源易造成资源泄露。

示例:

fun(
    shared_ptr<T>(new T),
    shared_ptr<T>(new T)   // Non-compliant, potential memory leak
);

例中 fun 函数的两个参数均包含 new 表达式,而参数的求值顺序在标准中是未声明的,出于优化等目的,可能会先为两个 T 类对象分配内存,之后再分别执行对象的构造函数,如果某个构造函数抛出异常,已分配的内存就无法回收了。

从 C++17 开始,参数的求值过程不再有所重叠,示例代码的问题在 C++17 后会有所缓解,但为了更广泛的适用性和兼容性,应避免在表达式中多次显式分配资源。

应改为:

shared_ptr<T> a{new T};   // Compliant
shared_ptr<T> b{new T};   // Compliant
fun(a, b);

这样即使构造函数抛出异常也会自动回收已分配的内存。

更好的方法是避免显式资源分配,用 make_shared、make_unique 等函数代替 new 运算符:

fun(
    make_shared<T>(),
    make_shared<T>()    // Compliant, safe and brief
);


相关

ID_memoryLeak

依据

ISO/IEC 14882:2003 5.2.2(8)-unspecified
ISO/IEC 14882:2011 5.2.2(8)
ISO/IEC 14882:2017 8.2.2(5)

参考

C++ Core Guidelines R.13



▌R2.18 流式资源对象不应被复制

ID_copiedStream       :drop_of_blood: resource warning


FILE 等流式对象不应被复制,如果存在多个副本会造成数据不一致的问题。

示例:

FILE f;
FILE* fp = fopen(path, "r");

f = *fp;                      // Non-compliant
memcpy(fp, &f, sizeof(*fp));  // Non-compliant


依据

ISO/IEC 9899:1999 7.19.3(6)
ISO/IEC 9899:2011 7.21.3(6)

参考

MISRA C 2012 22.5



▌R2.19 避免使用变长数组

ID_variableLengthArray       :drop_of_blood: resource warning


使用变长数组(variable length array)可以在栈上动态分配内存,但分配失败时难以通过标准方法控制程序的行为。

变长数组由 C99 标准提出,不在 C++ 标准之内,在 C++ 代码中不应使用。

示例:

void foo(int n)
{
    int a[n];   // Non-compliant, a variable length array
                // Undefined behavior if n <= 0
    ....
}

例中数组 a 的长度为变量,其内存空间在运行时动态分配,如果 n 不是合理的正整数会导致未定义的行为。

另外,对于本应兼容的数组类型,如果长度不同也会导致未定义的行为,如:

void bar(int n)
{
    int a[5];
    typedef int t[n];   // Non-compliant, a variable length array type
    t* p = &a;          // Undefined behavior if n != 5
    ....
}


相关

ID_stackAllocation

依据

ISO/IEC 9899:1999 6.7.5.2(5)
ISO/IEC 9899:2011 6.7.6.2(5)

参考

MISRA C 2012 18.8



▌R2.20 避免使用在栈上动态分配内存的函数

ID_stackAllocation       :drop_of_blood: resource warning


alloca、strdupa 等函数可以在栈上动态分配内存,但分配失败时难以通过标准方法控制程序的行为。

在栈上动态分配内存的函数可能效率更高,分配的内存也不用显式回收,但无法满足分配需求时会直接导致运行时错误,对其返回值的检查是无效的。应避免使用这种后果难以控制的函数,尤其在循环和递归调用过程中更不应使用这种函数,而且这种函数不是标准函数,依赖平台和编译器的具体实现。

示例:

#include <alloca.h>  // Or use malloc.h in MSVC

void fun(size_t n) {
    int* p = (int*)alloca(n * sizeof(int));  // Non-compliant
    if (!p) {
        return;  // Invalid
    }
    ....
}

例中 alloca 函数在栈上分配内存,如果 n 过大会使程序崩溃。



相关

ID_variableLengthArray
ID_invalidNullCheck

参考

CWE-770
SEI CERT MEM05-C



▌R2.21 局部数组不应过大

ID_unsuitableArraySize       :drop_of_blood: resource warning


局部数组在栈上分配空间,如果占用空间过大会导致栈溢出错误。

应关注具有较大数组的函数,评估其在运行时的最大资源消耗是否符合执行环境的要求。

示例:

void foo() {
    int arr[1024][1024][1024];   // Non-compliant, too large
    ....
}

在栈上分配空间难以控制失败情况,如果条件允许可改在堆上分配:

void foo() {
    int* arr = (int*)malloc(1024 * 1024 * 1024 * sizeof(int));   // Compliant
    if (arr) {
        ....     // Normal procedure
    } else {
        ....     // Handle allocation failures
    }
}


配置

maxLocalArraySize:函数内局部数组空间之和的上限,超过则报出

参考

CWE-770
SEI CERT MEM05-C



▌R2.22 避免不必要的内存分配

ID_unnecessaryAllocation       :drop_of_blood: resource warning


对单独的基本变量或只包含少量基本变量的对象不应使用动态内存分配。

示例:

bool* pb = new bool;   // Non-compliant
char* pc = new char;   // Non-compliant

内存分配的开销远大于变量的直接使用,而且还涉及到回收问题,是得不偿失的。

应改为:

bool b = false;   // Compliant
char c = 0;       // Compliant

用 new[] 分配数组时方括号被误写成小括号,或使用 unique_ptr 等智能指针时遗漏了数组括号也是常见笔误,如:

int* pi = new int(32);            // Non-compliant
auto ui = make_unique<int>(32);   // Non-compliant

应改为:

int* pi = new int[32];              // Compliant
auto ui = make_unique<int[]>(32);   // Compliant

有时可能需要区分变量是否存在,用空指针表示不存在,并通过资源分配创建变量的方式属于低效实现,不妨改用变量的特殊值表示变量的状态,在 C++ 代码中也可使用 std::optional 实现相关功能。



相关

ID_dynamicAllocation



▌R2.23 避免分配大小为零的内存空间

ID_zeroLengthAllocation       :drop_of_blood: resource warning


当申请分配的内存空间大小为 0 时,malloc、calloc、realloc 等函数的行为是由实现定义的。

示例:

int n = user_input();
if (n >= 0) {
    int* p = (int*)malloc(n * sizeof(int));   // Non-compliant
    if (p == NULL)
        log("Required too much memory");   // ‘n’ may also be zero
    else
        ....
}

当例中 n 为 0 时,malloc 可能会分配元素个数为 0 的数组,也可能会返回空指针。

又如:

int* p = (int*)malloc(n * sizeof(int));
....
realloc(p, 0);   // Non-compliant, use free(p) instead

C90 规定当 realloc 函数的长度参数为 0 时会释放内存,与 free§ 相同,但在后续标准中废弃了这一特性,不应继续使用。

这种情况下 C++ 语言的 new 运算符会分配元素个数为 0 的数组,但这种数组往往没有实际价值,而且要注意,在 C 和 C++ 语言中元素个数为 0 的数组也需要被释放。



依据

ISO/IEC 9899:1990 7.10.3.4
ISO/IEC 9899:1999 7.20.3(1)-implementation
ISO/IEC 9899:2011 7.22.3(1)-implementation
ISO/IEC 14882:2003 5.3.4(7)
ISO/IEC 14882:2011 5.3.4(7)

参考

SEI CERT MEM04-C



▌R2.24 避免动态内存分配

ID_dynamicAllocation       :drop_of_blood: resource warning


标准库提供的动态内存分配方法,其算法或策略不在使用者的控制之内,很多细节是标准没有规定的,而且也是内存耗尽等问题的根源,有高可靠性要求的嵌入式系统应避免动态内存分配。

在内存资源有限的环境中,由于难以控制具体的分配策略,很可能会导致已分配的空间用不上,未分配的空间不够用的情况。而在资源充足的环境中,也应尽量避免动态分配,如果能在栈上创建对象,就不应采用动态分配的方式,以提高效率并降低资源管理的复杂性。

示例:

void foo() {
    std::vector<int> v;   // Non-compliant
    ....
}

例中 vector 容器使用了动态内存分配方法,容量的增长策略可能会导致内存空间的浪费,甚至使程序难以稳定运行。



依据

ISO/IEC 9899:1999 7.20.3
ISO/IEC 9899:2011 7.22.3

参考

C++ Core Guidelines R.5
MISRA C 2004 20.4
MISRA C 2012 21.3
MISRA C++ 2008 18-4-1



▌R2.25 判断资源分配函数的返回值是否有效

ID_nullDerefAllocRet       :drop_of_blood: resource warning


malloc 等函数在分配失败时返回空指针,如果不加判断直接使用会导致标准未定义的行为。

在有虚拟内存支持的平台中,正常的内存分配一般不会失败,但申请内存过多或有误时(如参数为负数)也会导致分配失败,而对于没有虚拟内存支持的或可用内存有限的嵌入式系统,检查分配资源是否成功是十分重要的,所以本规则应该作为代码编写的一般性要求。

库的实现更需要注意这一点,如果库由于分配失败而使程序直接崩溃,相当于干扰了主程序的决策权,很可能会造成难以排查的问题,对于有高可靠性要求的软件,在极端环境中的行为是需要明确设定的。

示例:

void foo(size_t n) {
    char* p = (char*)malloc(n);
    p[n - 1] = '\0';              // Non-compliant, check ‘p’ first
    ....
}

示例代码未检查 p 的有效性便直接使用是不符合要求的。



依据

ISO/IEC 9899:1999 7.20.3(1)
ISO/IEC 9899:2011 7.22.3(1)

参考

CWE-252
CWE-476



▌R2.26 在 C++ 代码中禁用 C 资源管理函数

ID_forbidMallocAndFree       ⛔️ resource warning


为了简化资源管理并避免潜在的错误,在 C++ 代码中不应直接使用分配、释放普通指针的函数,而应使用容器、智能指针和相关工厂函数。

示例:

void foo(size_t n) {
    int* p = (int*)malloc(n * sizeof(int));  // Non-compliant
    ....
    free(p);  // Non-compliant
}

应改为:

void foo(size_t n) {
    auto p = make_unique<int[]>(n);  // Compliant
    ....
}


相关

ID_ownerlessResource

参考

C++ Core Guidelines R.10



  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TrustZone_Hcoco

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值