[译]CERT Secure Coding Standard — C语言安全编程规范(4)

Posted by c4pr1c3 on February 6, 2011

11. 信号

规则/建议条目全称 例外 笔记 /备注/点评
SIG00-C. 屏蔽由不可中断信号处理函数处理的信号 POSIX标准建议使用sigaction(),废弃signal()。但sigaction()不是C99标准。
SIG01-C. 理解信号处理函数持久化相关的实现细节 *nix平台上的信号处理函数一般是实现为“持久化”的,即除非显式的删除该信号处理函数,否则该信号处理函数会一直运行时有效。而Windows平台上则恰好相反,信号处理函数被执行一次后就会自动被删除。
SIG02-C. 避免使用信号机制来实现普通函数功能 从代码可安全移植的角度设计的本条建议。
SIG30-C. 在信号处理函数内部仅调用异步安全函数 查手册确定异步安全函数有哪些。
SIG31-C. 禁止在信号处理函数内部访问或修改共享对象 否则会导致竞争读写问题,引起数据状态的不一致。读写volatile sig_atomic_t类型变量则没有问题,读写其他类型变量的行为则是“未定义”的。
SIG32-C. 在信号处理函数内部禁止调用longjmp() 本条规则和SIG30-C. 在信号处理函数内部仅调用异步安全函数是相关的。
SIG33-C. 禁止递归调用raise()
SIG34-C. 禁止在可中断信号处理函数内调用signal() SIG34-EX1: 在支持持久化信号处理函数的平台上信号处理函数可以安全的修改自己的信号处理行为。 一个信号处理函数仅仅在其不需要是异步安全时可以嵌套调用signal()。
SIG35-C. 禁止从SIGSEGV, SIGILL, 或SIGFPE的信号处理函数返回 否则会引起“未定义”行为。

12. 错误处理

规则/建议条目全称 例外 笔记 /备注/点评
ERR00-C. 采用和实现一个一致和全面的错误处理策略
ERR01-C. 使用ferror()而不是errno来检查文件流错误 errno仅仅在某些库函数调用出错返回错误代码并会设置errno位时才有意义,并非所有的文件流操作操作都会设置errno。ferror()则是针对特定文件流是否操作出错的检查。
ERR02-C. 避免“带内”(in-band)错误指示符 ERR02-EX1: 空指针是另一个in-band错误指示符的例子,但空指针不会引起任何副作用。</p>

ERR02-EX2: 如果你能保证函数在执行过程中出错时绝不会继续执行(可以通过调用abort()或者longjmp()中断函数执行过程),你的函数就可以返回in-band错误指示符。

所谓in-band,学过通信的都知道所谓“带内”信号和“带外”信号,in-band的含义就是所传递的信息会占用“正常”的传输通道资源。在C语言的标准库函数实现里大量的使用了in-band错误指示符,即函数的返回值在正常执行成功时会返回一些操作成功相关的变量值,函数执行出错时又利用返回值设置错误变量值。相当于函数的返回值在这里既是正常“返回结果”的传递通道,又是错误“返回结果”的传递通道。由于两种返回结果可能是不同的数据类型,因此函数在定义时可能会使用“兼容”数据类型,从而增加了函数调用者调用函数时的逻辑处理复杂度,可能会导致一些“二义性”或者“理解偏差”错误。
ERR03-C. 调用TR24731-1标准定义的系列函数时使用运行时约束条件处理函数 strcpy_s()就是TR24731-1定义的一个函数,所谓“运行时约束条件处理函数”就是指的调用set_constraint_handler_s()注册一个运行时约束条件违反处理函数。该条建议主要针对微软平台,*nix平台忽略该条建议应该问题不大。
ERR04-C. 选择一个合适的程序退出策略 C99标准提供了4种程序退出策略:exit()、从main()函数返回、_Exit()和abort()。这四个函数的退出时动作差别如下表总结所示:</p>
函数名 关闭所有打开的流 flush所有的流缓冲 删除所有临时文件 调用atexit()注册的处理函数 程序退出状态
abort() 异常退出
_Exit() 正常退出
exit() 正常退出
从main()返回 正常退出
ERR05-C. 应用程序独立的代码应提供错误检测而不需强制错误处理 应用程序独立的代码包括:</p>
  • 编译器或操作系统提供
  • 来自第三方库
  • 自己开发的

应用程序专属代码在检测到错误时可以立刻进行错误处理,而应用程序独立的代码则无法在特定应用程序出错时提供相应的错误处理方法。但必须要能检测错误并向应用程序中的调用者报告错误。错误检测和报告方法包括:

  • 返回值(特别是errno_t类型返回值)
  • 一个地址参数
  • 一个全局对象(例如errno)
  • longjmp()
  • 以上方法的组合
ERR06-C. 理解assert()和abort()的退出行为 assert()宏会在执行时被展开为一个void表达式,并且在表达式的值为false时,在标准错误输出上给出相关错误信息,然后调用abort()终止程序运行。由于调用了abort(),atexit()注册的退出清理函数不会被调用执行,因此建议在可能的条件下调用static_assert宏而不是运行时assert宏。
ERR07-C. 优先使用支持错误检测机制的函数 例如优先使用strtol()而不是atoi()。
ERR30-C. 调用可能会出错并设置errno的库函数前先将errno置0,并在该函数返回值表示出错时再检查errno
ERR31-C. 禁止重新定义errno 该条规则已经被DCL37-C. 不要使用预留实现的标识符MSC38-C. 禁止访问会引起未定义行为的库对象或函数 代替
ERR32-C. 禁止依赖不确定的errno值
ERR33-C. 检测并处理错误

13. 应用程序编程接口

提示:本节的官方文档还处于“建设期”,故暂不提供翻译。

规则/建议条目全称 例外 笔记 /备注/点评
API00-C. Functions should validate their parameters
API02-C. Functions that read or write to or from an array should take an argument to specify the source or target size
API03-C. Create consistent interfaces and capabilities across related functions
API04-C. Provide a consistent and usable error checking mechanism
API07-C. Enforce type safety
API08-C. Protect header prototypes from misinterpretation
API09-C. Compatible values should have the same type
DCL09-C. Declare functions that return errno with a return type of errno_t

14. 并发

规则/建议条目全称 例外 笔记 /备注/点评
CON00-C. 避免多线程的竞争条件 CON01-EX1: 允许取消运行的线程一般都会提供一个清理函数,在清理函数中释放互斥锁是允许的行为。
CON01-C. 在同一个模块、同一个抽象层次获得和释放同步原语
CON31-C. 禁止解锁或删除其他线程的互斥锁
CON32-C. 当多线程访问相同数据时,使用互斥锁并确保没有邻近数据也被访问到 当多线程访问相同变量时,由于内存数据表示的原因,可能会出现在同一个地址空间(一个字节)中同时存在多个变量值的问题(特别是多个bit类型变量共享同一个字节的存储空间)。因此,当一个或多个连续字节地址空间被互斥锁锁定时,可能会出现“误锁定”的情况(锁定一个变量的同时,锁定了目标变量所在地址空间的其他变量。后续其他线程试图访问“看上去没有加锁”的其他变量时,出现访问失败或加锁失败的“意外”情况)。针对此问题没有通用的跨平台解决方案,一般情况下建议将一个并发访问变量“嵌入”在一个共用体中,并且在共用体中同时嵌入一个long类型变量(目的是填充内存地址空间,保证共享变量所在内存地址附近不会再有其他变量)。以下就是一个“安全”共享变量的声明实例:</p>
struct multi_threaded_flags {
  volatile unsigned int flag1 : 2;
  volatile unsigned int flag2 : 2;
};

union mtf_protect {
  struct multi_threaded_flags s;
  long padding;
};
CON33-C. 使用库函数时避免竞争条件 主要指的是多线程程序避免调用非“可重入安全”的库函数。典型的非“可重入安全”的库函数包括:</p>
    rand()
    getenv()
    strtok()
    strerror()
    asctime()
    ctime()
CON34-C. 声明多线程共享对象时要注意使用恰当的存储周期 A线程访问其他线程的栈变量或线程本地变量时会导致非法内存访问错误。
CON35-C. 使用预定义的加锁顺序来避免死锁 死锁的产生需要四个条件同时满足:</p>
  • 互斥访问
  • 保持并且等待
  • 无抢占
  • 循环等待

因此,预防死锁发生只需要破坏上述任意一个死锁必要条件即可。本规则建议“使用预定义的加锁顺序来避免死锁”。

CON36-C. 当持有锁时禁止执行可能会阻塞的操作 CON36-EX1: 线程在持有一个或多个锁的同时又在等待另一个锁时会阻塞. 当获取多个锁时, 加锁顺序必须避免死锁, 如CON35-C. 使用预定义的加锁顺序来避免死锁 阻塞操作包括但不限于:网络、文件和终端I/O。
CON37-C. 禁止使用超过1个互斥锁于一个条件变量的并发等待操作 pthread_cond_wait()和pthread_cond_timedwait()接受一个条件变量和一个互斥锁作为参数,并在条件变量被信号“唤醒”时先解锁再在运行前重新加锁。当一个线程A在等待特定的条件变量被满足和获得互斥锁时,另一个线程B可能已经持有该互斥锁并且只是在等待条件变量满足。但如果另一个线程B是持有另一个互斥锁的同时等待相同的条件变量被满足,则该行为是“未定义”的。
CON38-C. 通知所有在等待同一个条件变量的线程而不是某一个线程 避免死锁。
CON39-C. 无锁编程时避免ABA问题 无锁编程指的是不使用显式的锁来同步更新一个共享数据结构。这种方法可以确保任意一个线程都不会被阻塞。该方法的优点体现在:</p>
  • 可以应用于无法使用锁的场景,例如中断处理函数
  • 相比较于基于锁的算法,在某些场景下更有优势,如多处理器环境下的扩展性会更好
  • 避免实时系统中的优先级交错问题

局限性体现在:

  • 需要特殊的CPU原子操作指令支持,如 CAS (compare and swap)或LL/SC (load linked/store conditional)

应用场景:

  • Linux 2.5内核引入的“Read-copy-update” (RCU)
  • AMD多核系统上的无锁编程

ABA问题指的是在数据同步过程中,一个变量值被先后读取了两次,并且两次读取结果完全相同。但实际上在这两次读取之间的时间片段里,另一个线程先修改了该变量值,然后做了些其他工作后又将该值修改回去。如果是内存分配或指针相关操作,则第二次读取操作有可能会出现“访问已释放的内存”错误。
避免ABA问题的方法可以使用加锁或者hazard指针(唯一写,并发读)。