C++安全编码摘录
1原文:
说明:
Noncompliant Code Example(NCE) :不符合安全编码规则的代码示例
Compliant Solution(CS) :符合安全规则的解决办法
2 Declarations and Initialization(DCL)
DCL53 Do not write syntactically ambiguous declarations
NCE
#include <iostream> struct Widget { explicit Widget(int i) { std::cout << "Widget constructed" << std::endl; } }; struct Gadget { explicit Gadget(Widget wid) { std::cout << "Gadget constructed" << std::endl; } }; void f() { int i = 3; Gadget g(Widget(i)); //被解析为一个函数声明 std::cout << i << std::endl; }
int main(){ return 0; } |
运行结果
3 |
CS
#include <iostream> struct Widget { explicit Widget(int i) { std::cout << "Widget constructed" << std::endl; } }; struct Gadget { explicit Gadget(Widget wid) { std::cout << "Gadget constructed" << std::endl; } }; void f() { int i = 3; Gadget g1((Widget(i))); // Use extra parentheses Gadget g2{Widget(i)}; // Use direct initialization std::cout << i << std::endl; } int main(){ return 0; } |
运行结果
Widget constructed Gadget constructed Widget constructed Gadget constructed 3 |
DCL56 Avoid cycles during initialization of static objects
NCE
// file.h #ifndef FILE_H #define FILE_H class Car { int numWheels; public: Car() : numWheels(4) {} explicit Car(int numWheels) : numWheels(numWheels) {} int get_num_wheels() const { return numWheels; } }; #endif // FILE_H // file1.cpp #include "file.h" #include <iostream> extern Car c; static int numWheels = c.get_num_wheels(); int main() { std::cout << numWheels << std::endl; } // file2.cpp #include "file.h" Car get_default_car() { return Car(6); } Car c = get_default_car(); |
运行结果
如果g++ -std=c++11 -o test file1.cpp file2.cpp,那么结果是0.即调用c.get_num_wheels()时,对象c中的数据成员还没有被初始化。
如果g++ -std=c++11 -o test file2.cpp file1.cpp,那么结果是6.结果和编译顺序相关了。
|
CS
The global object c is initialized before execution of main() begins, so by the time get_num_wheels() is called, c is guaranteed to have already been dynamically initialized.
将调用放到main中,此时外部全局对象已经完成初始化。
// file.h #ifndef FILE_H #define FILE_H class Car { int numWheels; public: Car() : numWheels(4) {} explicit Car(int numWheels) : numWheels(numWheels) {} int get_num_wheels() const { return numWheels; } }; #endif // FILE_H // file1.cpp #include "file.h" #include <iostream> int &get_num_wheels() { extern Car c; static int numWheels = c.get_num_wheels(); return numWheels; } int main() { std::cout << get_num_wheels() << std::endl; } // file2.cpp #include "file.h" Car get_default_car() { return Car(6); } Car c = get_default_car(); |
Car c是不是应该默认初始化,然后是赋值操作?
运行环境中,Car c直接由explicit构造。不是先默认初始化,然后赋值。Car c = get_default_car(); 这个直接由explicit构造函数构造了。
DCL57 Do not let exceptions escape from destructors or deallocation functions
However, the C++ Standard, [except.handle], paragraph 15 [ISO/IEC 14882-2014], in part, states
the following:
The currently handled exception is rethrown if control reaches the end of a handler of
the function-try-block of a constructor or destructor.
C++标准说异常会如果能到达构造、析构函数的最后,那么会重新抛出。
以下假如Bad类在析构时可能会抛出异常,Bad的代码又改不了,那么我们的类怎么修改这个安全风险。
NCE
class SomeClass { Bad bad_member; public: ~SomeClass() { try { // ... } catch(...) { // Handle the exception thrown from the Bad destructor. } } }; |
CS
class SomeClass { Bad bad_member; public: ~SomeClass() { try { // ... } catch(...) { // Catch exceptions thrown from noncompliant destructors of // member objects or base class subobjects. // NOTE: Flowing off the end of a destructor function-try-block // causes the caught exception to be implicitly rethrown, but // an explicit return statement will prevent that from // happening. return; } } }; |
DCL59 Do not define an unnamed namespace in a header file
在某头文件中定义了匿名的命名空间,且包含变量或函数。那么包含这个头文件的编译单元中,将各自生成这些变量或函数的实例。
3 Expressions (EXP)
EXP57 Do not cast or delete pointers to incomplete classes
前向声明中会引用不完全类型的对象,但是要注意:
不要尝试删除指向不完整类类型的对象的指针;
不要尝试转换指向不完整类类型的对象的指针;
If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.
NCE
class Handle { class Body *impl; // Declaration of a pointer to an incomplete class public: ~Handle() { delete impl; } // Deletion of pointer to an incomplete class // ... }; |
CS
In this compliant solution, the deletion of impl is moved to a part of the code where Body is
defined.
如果有前向声明,且有释放不完整类,那么释放的定义在不完整类定义的后面。
class Handle { class Body *impl; // Declaration of a pointer to an incomplete class public: ~Handle(); // ... }; // Elsewhere class Body { /* ... */ }; Handle::~Handle() { delete impl; } |
6 Characters and Strings (STR)
STR51Do not attempt to create a std::string from a null pointer
不要用空指针创建string
NCE
#include <cstdlib> #include <string> void f() { std::string tmp(std::getenv("TMP")); if (!tmp.empty()) { // ... } } |
CS
#include <cstdlib> #include <string> void f() { const char *tmpPtrVal = std::getenv("TMP"); std::string tmp(tmpPtrVal ? tmpPtrVal : ""); if (!tmp.empty()) { // ... } } |
STR52 Use valid references, pointers, and iterators to reference elements of a basic_string
和vector insert一样,指针可能因为扩容而变更。
NCE
#include <string> void f(const std::string &input) { std::string email; // Copy input into email converting ";" to " " std::string::iterator loc = email.begin(); for (auto i = input.begin(), e = input.end(); i != e; ++i, ++loc) { email.insert(loc, *i != ';' ? *i : ' '); } } |
CS
#include <string> void f(const std::string &input) { std::string email; // Copy input into email converting ";" to " " std::string::iterator loc = email.begin(); for (auto i = input.begin(), e = input.end(); i != e; ++i, ++loc) { loc = email.insert(loc, *i != ';' ? *i : ' '); } } |
STR53 Range check element access
下标引用需要检查范围
NCE
#include <string> extern std::size_t get_index(); void f() { std::string s("01234567"); s[get_index()] = '1'; } |
CS
#include <stdexcept> #include <string> extern std::size_t get_index(); void f() { std::string s("01234567"); try { s.at(get_index()) = '1'; } catch (std::out_of_range &) { // Handle error } } |
7 Memory Management (MEM)
MEM50 Do not access freed memory
局部变量在出if就释放了
NCE
#include <iostream> #include <memory> #include <cstring> int main(int argc, const char *argv[]) { const char *s = ""; if (argc > 1) { enum { BufferSize = 32 }; try { std::unique_ptr<char[]> buff(new char[BufferSize]); std::memset(buff.get(), 0, BufferSize); // ... s = std::strncpy(buff.get(), argv[1], BufferSize - 1); } catch (std::bad_alloc &) { // Handle error } } std::cout << s << std::endl; } |
CE
#include <iostream> #include <memory> #include <cstring> int main(int argc, const char *argv[]) { std::unique_ptr<char[]> buff; const char *s = ""; if (argc > 1) { enum { BufferSize = 32 }; try { buff.reset(new char[BufferSize]); std::memset(buff.get(), 0, BufferSize); // ... s = std::strncpy(buff.get(), argv[1], BufferSize - 1); } catch (std::bad_alloc &) { // Handle error } } std::cout << s << std::endl; } |
9 Exceptions and Error Handling (ERR)
ERR51 Handle all exceptions
CS
#include <thread> void throwing_func() noexcept(false); void thread_start(void) { try { throwing_func(); } catch (...) { // Handle error } } void f() { std::thread t(thread_start); t.join(); } |
ERR53 Do not reference base classes or class data members
When an exception is caught by a function-try-block handler in a constructor, any fully
constructed base classes and class members of the object are destroyed prior to entering the
handler.
构造函数用try-block处理异常之前,类中成员对象已经销毁了。异常处理函数中不要操作这些数据成员。
NCE
#include <string> class C { std::string str; public: C(const std::string &s) try : str(s) { // ... } catch (...) { if (!str.empty()) { // ... } } }; |
ERR54 Catch handlers should order their parameter types from most derived to least derived
CS由深到浅
class B {}; class D : public B {}; void f() { try { // ... } catch (D &d) { // ... } catch (B &b) { // ... } } |
10 Object Oriented Programming (OOP)
OOP50 Do not invoke virtual functions from constructors or destructors
禁止在构造函数或析构函数中使用虚函数,在构造期间尝试从基类中调用子类的函数有风险的。此时,子类还没有完成初始化。
OOP51 Do not slice derived objects
子类继续父类,子类可能增加其他变量。如果以值的方式赋值或拷贝子类对象给父类对象,则附加的信息丢失。禁止使用子类对象初始化一个父类对象,除非通过引用,指针。
NCE
#include <iostream> #include <string> class Employee { std::string name; protected: virtual void print(std::ostream &os) const { os << "Employee: " << get_name() << std::endl; } public: Employee(const std::string &name) : name(name) {} const std::string &get_name() const { return name; } friend std::ostream &operator<< (std::ostream &os, const Employee &e) { e.print(os); return os; } }; class Manager : public Employee { Employee assistant; protected: void print(std::ostream &os) const override { os << "Manager: " << get_name() << std::endl; os << "Assistant: " << std::endl << "\t" << get_assistant() << std::endl; } public: Manager(const std::string &name, const Employee &assistant) : Employee(name), assistant(assistant) {} const Employee &get_assistant() const { return assistant; } }; void f(Employee e) { std::cout << e; } int main() { Employee coder("Joe Smith"); Employee typist("Bill Jones"); Manager designer("Jane Doe", typist); f(coder); f(typist); f(designer); } |
CE
// Remainder of code unchanged... void f(const Employee *e) { if (e) { std::cout << *e; } } int main() { Employee coder("Joe Smith"); Employee typist("Bill Jones"); Manager designer("Jane Doe", typist); f(&coder); f(&typist); f(&designer); } |
OOP52 Do not delete a polymorphic object without a virtual destructor
当父类没有定义虚析构函数时,尝试使用指向父类的指针删除子类对象将导致未定义行为。
11 Concurrency (CON)
CON50-CPP. Do not destroy a mutex while it is locked
std::mutex m;该对象需要在需要它的周期内不能被销毁。
以下情形中对象m,在退出start_threads后被销毁,此时线程处理函数do_work可能还需要用,所以存在风险。
#include <mutex> #include <thread> const size_t maxThreads = 10; void do_work(size_t i, std::mutex *pm) { std::lock_guard<std::mutex> lk(*pm); // Access data protected by the lock. } void start_threads() { std::thread threads[maxThreads]; std::mutex m; for (size_t i = 0; i < maxThreads; ++i) { threads[i] = std::thread(do_work, i, &m); } } |
解决方法:扩大mutex对象的作用域;使用join,让线程处理完成后再回到调用处。
CON51-Ensure actively held locks are released on exceptional conditions
如果在lock()和unlock()之间出现异常,异常处理中要释放锁。
另外推荐用RAII封装的lock_guard,unique_lock用一个互斥对象来初始化。
CON52-CPP. Prevent data races when accessing bit-fields from multiple threads
同一字节的不同bit位被多线程访问需要加锁保护。
但以下一个结构体中两个不同成员,可以分别在两个线程中独立访问而不用加锁。
struct MultiThreadedFlags { unsigned char flag1; unsigned char flag2; }; MultiThreadedFlags flags; void thread1() { flags.flag1 = 1; } void thread2() { flags.flag2 = 2; } |
CON53-CPP. Avoid deadlock by locking in a predefined order
两个互斥对象如何避免死锁
方法一:每个线程按照顺序加锁
比如std::mutex m1; std::mutex m2
每个线程都用先上锁m1,再上锁m2的顺序。
std::lock_guard<std::mutex> firstLock(m1);
std::lock_guard<std::mutex> firstLock(m2);
方法二:unique_lock + lock()
// Create lock objects but defer locking them until later. std::unique_lock<std::mutex> lk1( from->balanceMutex,std::defer_lock); std::unique_lock<std::mutex> lk2( to->balanceMutex, std::defer_lock); // Lock both of the lock objects simultaneously. std::lock(lk1, lk2); |
CON54-CPP.Wrap functions that can spuriously wake up in a loop
对于std::condition_variable类的wait、wait_for、wait_until,可能受系统伪激活的影响。
所以对于wait的条件,需要用while循环来检查,而不能用单次if来判断。
while (until_finish()) { // Predicate does not hold. condition.wait(lk); } |
或者用lambda表达式
#include <condition_variable> #include <mutex> struct Node { void *node; struct Node *next; }; static Node list; static std::mutex m; static std::condition_variable condition; void consume_list_element() { std::unique_lock<std::mutex> lk(m); condition.wait(lk, []{ return list.next; }); // Proceed when condition holds. } |
CON55-CPP. Preserve thread safety and liveness when using condition variables
notify_one()时会随机唤醒一个阻塞状态的线程。在每个线程一个唯一条件变量的情况下使用。
notify_all 唤醒所有线程。
CON56-CPP. Do not speculatively lock a non-recursive mutex that is already owned by the calling thread
非递归互斥类(std::mutex,std::timed_mutex,std::shared_timed_mutex)
在本线程中,试图锁定一个已经加锁的对象,结果是不可预期的。
NCE
#include <mutex> #include <thread> std::mutex m; void do_thread_safe_work(); void do_work() { while (!m.try_lock()) { // The lock is not owned yet, do other work while waiting. do_thread_safe_work(); }try { // The mutex is now locked; perform work on shared resources. // ... // Release the mutex. }catch (...) { m.unlock(); throw; } m.unlock(); } void start_func() { std::lock_guard<std::mutex> lock(m); do_work(); } int main() { std::thread t(start_func); do_work(); t.join(); } |