C++安全编码摘录

1原文:

https://resources.sei.cmu.edu/downloads/secure-coding/assets/sei-cert-cpp-coding-standard-2016-v01.pdf

 

说明:

Noncompliant Code ExampleNCE) :不符合安全编码规则的代码示例

Compliant SolutionCS) :符合安全规则的解决办法

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(){
f();

    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(){
f();

    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();

 

运行结果

  1. 即使顺序不对,是不是应该返回4?而不是0?

  如果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_guardunique_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类的waitwait_forwait_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();

}

posted on 2020-01-13 20:15  SunnyPoem  阅读(718)  评论(0编辑  收藏  举报