Giter Club home page Giter Club logo

safe-rules's Introduction

360 安全规则集合

Version License

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

  《安规集》适用于桌面、服务端及嵌入式软件系统,提供:

    c-cpp-rules.md:C/C++ 规则详细说明文档
    c-cpp-rules.json:C/C++ 规则结构化文档
    c-ub-list.md:C 未定义行为成因列表
    cpp-ub-list.md:C++ 未定义行为成因列表

  各文档的网页版本请参见: https://saferules.github.io/

  传达语言标准,融汇行业标准,积极推广安全编程理念是《安规集》的宗旨。欢迎提供修订意见和扩展建议,由于《安规集》的文档是自动生成的,请不要直接编辑各文档,可在 Issue 区提出意见,管理员录入数据库后会在专门的“致谢列表”中铭记您的贡献。

  可在 Apache-2.0 协议许可范围内随意传播《安规集》相关内容,也可以适当取舍某些规则以适应自身需求。

safe-rules's People

Contributors

brotherbeer avatar mq-b avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

safe-rules's Issues

递归调用析构函数是UB

ID_this_deleteInDestructor只说了无限递归,没说UB的事

Once a destructor is invoked for an object, the object no longer exists; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended

`R3.1.2` “include 指令中禁用不合规的字符”中的建议描述,不妥

最后一句话,原文:

另外,某些平台的文件路径不区分大小写,建议在头文件名称中只使用小写字母以提高可移植性

windows 的确不区分大小写,以至于当我想 include<vector> 的时候,实际也可以写成 include<VecTor>(当然,这毫无意义)。我们要说的是“建议在头文件名称中只使用小写字母以提高可移植性”,其实这基本是不可能的

  • 头文件的内容应该完全按照要引用的文件的格式

如果按照建议:“在头文件名称中只使用小写字母”:

// Ab.h
#include<iostream>
static void f1(){
    std::puts("Ab.h");
}
// main.cpp
#include"ab.h" // 小写

int main(){
    f1();
}
 g++ main.cpp -o main

那么此段代码只能在 windows 等不区分文件大小写的环境下通过编译,如果在 Linux 等区分的环境,则无法通过编译。

`R14.24` “指针运算应使用数组下标的方式” 是否没考虑 `p[0]`、`*p`

此规则是否没考虑过 * 运算符?

T* p = new T{};
*p;            // 合理
p[0];          // 完全等价于 *(p + 0)

内建下标表达式 E1[E2] 除了值类别(见下文)和求值顺序 (C++17 起)之外与表达式 *(E1 + E2) 严格等同

我个人认为,如果是像我给出的示例一样,只是指针指向了一个对象(使用 new 而非 new[]),使用 *,要远好于 [0]

这在于我认为使用 []暗示这是一个“数组”,或者说这个指针指向了一片连续的内存。而 *暗示这是一个指针

这种略微抽象但是很常见的意思。

这就如同,一个 int arr[10] 数组对象,我们要访问它的第一个元素,应该是 arr[0],而不是 *arr,虽然这也可行,但是暗示的意思很怪,阅读此代码下意识会认为这是个指针


说来说去,表达的意思也很简单,“指针运算应使用数组下标的方式”,这种规则,并不适合完全遵守,禁不起实际的推敲。

规则中提到的示例:

int a[10];
int* p;

p = a + 1;      // Non-compliant
p = &a[1];      // Compliant

*(p + 1) = 0;   // Non-compliant
p[1] = 0;       // Compliant

p = p + 1;      // Non-compliant
p++;            // Compliant
p = &p[1];      // Compliant

不够完善。

刚哥强

刚哥出品,必属精品!

360沉淀的C++的安全规范准则,是从多年的代码检查中总结归纳出来的,适合企业落地的规范,如今开源了,可喜可贺啊!

把寄存器赋值给指针在驱动里是正常的

safe-rules/c-cpp-rules.md

Lines 18543 to 18550 in d06e2da

### <span id="fixedaddrtopointer">▌R14.6 不应将非零常量值赋值给指针</span>
ID_fixedAddrToPointer&emsp;&emsp;&emsp;&emsp;&nbsp;:fire: pointer warning
<hr/>
固定地址是不可移植的,且存在安全隐患。

int*p=0x1234;这种在驱动里很正常,不然代码没法写了

我的建议是单独说明一下,嵌入式有关的可以排除

除零规则示例错误

void foo() {
size_t n = 0;
if (condition) {
n = bar();
}
return 100 / n; // Non-compliant, must determine whether ‘n’ is 0
}

不应返回void

几点疑问

右子表达不应有副作用,求余运算符左右子表达重复
这些少写了式字?

R9.6.2和R9.6.3说的是catch,但被分到了try一节
应该放到catch那节吧?

缺少 `signed char`

- char 或 unsigned char

根据 ISO/IEC 14882: 2003 3.9.1(1) 和 ISO/IEC 14882: 2011 3.9.1(1):

Plain char, signed char, and unsigned char are three distinct types

三者是完全不同的类型:

#include <type_traits>

static_assert(std::is_same<char, signed char>::value == false, "");
static_assert(std::is_same<signed char, unsigned char>::value == false, "");
static_assert(std::is_same<char, unsigned char>::value == false, "");

因此应该增加 signed char,从而改为:

- `char``signed char``unsigned char`

C++ 中只要有返回值的函数没有从 `return` 返回就是 UB

### <span id="_50">50. 函数没有通过 return 语句返回明确的值,但函数返回值被使用</span>

此处“但函数返回值被使用”叙述在 C 语言中是正确的,但是在 C++ 中有误。在 C++ 中,返回值不是 void 的函数只能从 return 语句返回,否则,无论是否使用返回值,都会产生 UB。C++03 和 C++11 标准 6.6.3(2) 的叙述:

Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.

并没有对返回值是否使用提出要求。关于此,一个经典的例子是:

#include <cstdio>

int func(int n) {
    for (int i = 0; i < n; ++i) {
        std::printf("%d\n", i);
    }
}

其在 -O3 优化等级编译产生汇编代码:https://gcc.godbolt.org/z/o6YrEx9WE
可以看到 for 直接被 GCC 和 Clang 优化成了死循环 jmp 指令,而非预期的进行 n 次循环(可能是由于没有 return 语句造成编译器找不到函数的返回点而默认 for 循环为死循环),符合未定义行为不可预期的特征。


但是在 C 语言中,返回值被使用时才会产生未定义行为。在 C11 标准(ISO/IEC 9899: 2011)6.9.1(12) 中规定:

If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.

且 GCC 和 Clang 都能给出符合预期的汇编代码:https://gcc.godbolt.org/z/9fqdT8eTb

可见原表述在 C 语言中正确,但是在 C++ 语言中有误。

“`R6.4.2` 禁用可变参数列表“中使用模板参数包的示例过时且糟糕,应该使用折叠表达式

示例如下:

template <class T, class ...Args>
void get_argstrs(vector<string>& vs, const T& arg, const Args& ...rest) {
    ostringstream oss;
    oss << arg;
    vs.emplace_back(oss.str());
    if constexpr(sizeof...(rest) > 0) {
        get_argstrs(vs, rest...);
    }
}

template <class ...Args>
string format(const char* fmt, const Args& ...args) {  // Compliant
    string res;
    if constexpr(sizeof...(args) > 0) {
        vector<string> vs;
        const size_t n = strlen(fmt);
        get_argstrs(vs, args...);
        for (size_t i = 0, j = 0; i < n; i++) {
            if ((fmt[i] == '@' || fmt[j] == '$') && j < vs.size()) {
                res.append(vs[j++]);
            } else {
                res.push_back(fmt[i]);
            }
        }
    }
    return res;
}

明明都用上 C++17 编译期 if 了,却还在使用这老式的递归给形参包解包的方式,完全可以使用折叠表达式

比如改写一下 get_argstrs

template <class ...Args>
void get_argstrs2(std::vector<std::string>& vs ,const Args& ...rest) {
    ((vs.emplace_back(std::to_string(rest))), ...);
}

运行结果

`R3.2.8` “不应使用宏定义常量” 中的正确示例可以改进

原文:

应改为:

namespace U {
   const float PI = 3.14F;  // Compliant
}

namespace V {
   const long double PI = 3.14159L;  // Compliant
}

const 替换为 constexpr 会更加合适。

主要是有几点考量:

  1. const 修饰的标量类型可以进行非常量初始化,

    int a = 1;
    const int n = a;
  2. const 修饰的标量类型,不一定是“常量表达式”,

    template<int a>
    int i_v{};
    
    const int i = 1; // 常量表达式初始化
    int b = 1;
    const int i2 = b;
    
    i_v<i>;         // i  是整形常量表达式 OK
    i_v<i2>;        // i2 不是整形常量表达式 Error

    如果更换为 constexpr 则没有这些问题了。

  3. 由于关键字复用和 C 语言的缘故,const 本身的含义不那么明确,它的要求也不是那么严格,如果确定是常量,在 C++ 中应该使用 constexpr

    原文使用了命名空间,默认 C++ 的语境。

C 头文件名不应被视为过时的

问题来自提交 69bba35

C++ 标准中将 C 头文件设为弃用是历史错误,因为大部分 C 头文件并不是待移除的候选。 C++23 更正了这个问题,见 WG21 P2340R1

现在的推荐应该是

The intended use of these headers is for interoperability only. It is possible that C++ source files need to include one of these headers in order to be valid ISO C. Source files that are not intended to also be valid ISO C should not use any of the C headers.

这些头文件仅有意用于可互操作。 C++ 源文件有可能需要包含这些头文件之一以成为合法的 ISO C。无意同时作为合法的 C 的源文件不应该使用任何这些 C 头文件。

`R2.9` “对象被移动后不应再被使用”描述与规则可以改进

原文:

std::move 宣告对象的数据即将被转移到其他对象,转移之后对象在逻辑上不再有效,不应再被使用。

单看这句话并无什么太过分的问题,但是主要在于:类似的话术传播非常广泛和深远,让人对于移动语义和 std::move 抱有了很多不切实际的幻想,认为它是编译器做了什么魔法,std::move 了就不能再使用

原文描述本身并无太大问题,但是我认为总归还是可以改进的,前半句是没有太多问题的,也是一种“君子协定”,让开发者们约定了,自己定义的类的移动构造、移动赋值等函数,是转移资源所有权。

这里的重点在于移动构造、移动赋值等函数,而非是 std::move 本身,std::move 实际几乎什么也没做,它只是将左值表达式转换到亡值表达式,用以匹配移动构造、移动赋值等函数

“移动”,是 std::move 的语义上的,而非实际作用上的。

原文描述的:“std::move 宣告对象的数据即将被转移到其他对象”,这里“宣告”,其实就是指的语义上的“移动”,一种暗示,我能明白其表达含义。


总而言之,我认为应该强调:

  1. 移动之后的对象,它的状态取决于移动构造、移动赋值等函数本身的实现,而非是编译器魔法,到底能否重新使用,也取决于各位的实现,通常来说,重新对对象进行初始化,是可以再次正确使用的。
  2. std::move 本身。

虽然这可能让此规则不那么简洁,但是对后人来说,大大减小难度,也可以避免中文互联上的无数的对移动语义的错误信息传播误导。

`R2.1` “不可失去对已分配资源的控制”线程 `detach` 理论上也算失去了控制?

失去对已分配资源的控制

其实单看这个,线程 detach() 也算是失去控制了。

std::thread::detach,线程对象释放了自己的资源(线程)的所有权,允许线程执行独立地持续。

不过可以保证一旦该线程退出,则释放任何分配的资源

并不会有原文中说的:

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

之类的问题。

或许是标题中的 “不可失去对已分配资源的控制”,中没有明确是“”失去了对资源的控制?

线程 detach() 之后,我们的确失去了对此线程的控制,之后的合法性由 OS 保证,这或许不是什么问题。

当然,我不是推崇 detach(),它本身也有不少问题,


总而言之,或许我觉得此规则可以增加一些描述,明确是“谁”失去了资源的控制,或者即使失去,如果有保证正确,也无所谓。

[笔误] R10.2.11 运算符`~=`

不小心提交了pr,然后才注意到主页说不要编辑文档,不过可以看下这部分的修正 #11

~=不是合法的运算符,此处应是笔误

补充volatile使用场景

safe-rules/c-cpp-rules.md

Lines 6502 to 6509 in d06e2da

应在适当的场景中合理使用 volatile,否则会导致优化或同步相关的多种问题。
只应在以下场景使用 volatile:
- 与汇编等语言混合处理同一对象
- 信号或中断处理函数处理共享对象
- 对象地址对应外设地址
- 出于安全目的清理内存中的对象

和longjmp有关的变量也得定义成volatile的,longjmp的man:

The values of automatic variables are unspecified after a call to longjmp() if they meet all the following criteria:
· they are local to the function that made the corresponding setjmp(3) call;
· their values are changed between the calls to setjmp(3) and longjmp(); and
· they are not declared as volatile.
Analogous remarks apply for siglongjmp().

#include <stdio.h>
#include <setjmp.h>

static jmp_buf jb;

void fn(void) { longjmp(jb, 1);}

int main(void)
{
  int i = 1;
  if (!setjmp(jb)) {
    i++;
    fn();
  } else {
    printf("%i\n", i);
  }
  return 0;
}

这段代码-O0 -O2输出不一样,就是因为i应该是volatile的,我知道有条规则是不让用longjmp,不过还是说明一下好

R2.20 windows 无法使用 `alloca.h` 标头

#include <alloca.h>

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

无法在 msvc 通过编译,找不到这个标头。

使用 malloc.h 才可以

在 msvc malloc.h 下的 alloca 是一个宏,看起来是用作兼容的。

#if defined(_CRT_INTERNAL_NONSTDC_NAMES) && _CRT_INTERNAL_NONSTDC_NAMES
    #define alloca _alloca
#endif
_Ret_notnull_ _Post_writable_byte_size_(_Size)
void* __cdecl _alloca(_In_ size_t _Size);

但是改成使用 malloc.h 的话, gcc 又不行了(mingw 的话可以),我觉得有必要稍微更改一下这个示例。

说明在不同的编译器它可能位于不同的头文件中。

以及有必要强调 alloca、strdupa 这种函数不是 C 标准库,也不是 POSIX,而是 GNU 的扩展 也被其他的编译器和库广泛支持。


alloca、strdupa 等函数可以在栈上动态分配内存,但分配失败时的行为不受程序控制。

其实我觉得倒也不是完全不受控制,可以说的清楚一些,只是处理很麻烦,调整栈指针事太多了。

比如 msvc 文档说的

如果在 try 块内调用 _alloca,则必须在 catch 块中调用 _resetstkoflw

alloca 就是不能用,不用 raii 谁保证你能调用 resetstkoflw,要考虑的事情太多了。

`SIG_ATOMIC_MIN` 与 `SIG_ATOMIC_MAX` 的问题

R15.1 避免异步信号处理产生的数据竞争

用 SIG_ATOMIC_MIN 和 SIG_ATOMIC_MAX 之间的值对 sig_atomic_t 类型的对象赋值可以保证原子性,超出范围的赋值,或赋值之外的操作不能保证原子性,需要避免。

这里描述存在误导。SIG_ATOMIC_MINSIG_ATOMIC_MAX 本来就是 sig_atomic_t 类型的最小值与最大值,不存在此范围外的值。如果将更大范围的整数值赋值给 sig_atomic_t,那么在赋值会发生隐式转换。

R5.1.21 "存在构造、析构的类" 描述不明确

存在构造、析构或虚函数的类不应采用 struct 关键字

C++标准规定:

如果没有对类类型提供任何用户声明的构造函数,那么编译器将始终声明一个作为它的类的 inline public 成员的默认构造函数。

如果不向类类型提供用户声明的析构函数,那么编译器总是会声明一个析构函数作为这个类的 inline public 成员。

原文想表达的应该是:存在用户显式定义的构造、析构或虚函数或虚函数的类不应采用 struct 关键字。


并且我认为,是用 class 还是 struct,这种依靠开发者们约定的额外意思,不单单是简单的构造析构。

普遍认同的几个约定是:

  • 如果有任何非公开成员,就使用 class 而不是 struct。
  • 如果类有一个需要在程序执行过程中永远保持成立的条件,就使用 class
struct Pair{  //成员可以独立变化,并且数据一定合法
    string name;
    int volume;
};

class Date{
public:
    //校验 {yy,mm,dd}是不是合法的日期并进行初始化
    Date(int yy, Month mm, char dd);
    // ...
private:
    int y;
    Month m;
    char d;    //
}

值得一提的是,很多库并没有很好的遵守,我们举例 QPoint源码

只能说,都是一些建议了。

`cpp-ub-list` `55.` 空指针解引用

示例

struct T {
   int i;

   int foo() { return i; }
   int bar() { return 0; }

   static int baz();
};

T* p = nullptr;

p->foo();   // Undefined behavior
p->bar();   // Undefined behavior
p->baz();   // Well-defined, ‘baz’ is a static member

p->baz() 为什么会是良定义?对于内建类型,表达式 E1->E2(*E1).E2 严格等价,任何指针类型都是内建类型。。

这里应该等价为:

(*p).baz();

(*p) 解引用空指针。

示例不一定会产生 UB

safe-rules/cpp-ub-list.md

Lines 1580 to 1589 in b70783c

### <span id="_53">53. 使用没有 volatile 限定的 glvalue 访问有 volatile 限定的对象</span>
<br/>
示例:
```
int foo(int&);
volatile int* bar();
foo((int&)*bar()); // Undefined behavior
```

该示例不一定会产生 UB,因为 bar 虽然返回 volatile int*,但不一定返回 volatile 限定的 int 对象,例如下面的代码:

int main() {
    int foo(int& x);
    volatile int* bar(void);
    foo((int&)*bar());  // 无 UB 产生
}

int g0 = 0;
int foo(int& x) { return x = 888; }
volatile int* bar(void) { return &g0; }

建议示例可以更加完善,例如给出文字说明,或者给出 bar 的实现,等等。

R8.16示例参数名称不对?

class A {
    int i;
    static int s;

public:
    A(): i(0) {}
    A(const A& rhs) { 
        i = a.i;     // Compliant          应该是i=rhs.i吧?
        s++;         // Non-compliant
        ++i;         // Non-compliant
    }
};

`R14.18` 的观点非常值得商榷,且解释并不合理

释放指针后应将指针赋值为空指针

这一原始的规则出现的非常的早,大多数时候也的确没问题,用“空”,表示当前指针的状态,此用法似乎可以追溯到四十年前。

但是教条主义没有价值。其实“释放指针后赋空”,本质上的意思只是因为,空指针字面量等东西可以直接给任何类型的指针,赋值比较方便,然后大家约定了以空表示状态。

事实上如果大家约定的话,完全可以做出一个 not_null,比如 gsl::not_null<T>,那么为什么要这么做呢?它能带来什么好处?

它的好处其实在文中对例子的描述就已经说明了:

dealloc 函数释放指针后将其赋值为空指针,delete、free 等接口可以接受空指针,dealloc 函数被反复调用也没有问题,即使相关接口不接受空指针,也可以使问题立即显现出来,便于修正。

delete、free 等接口可以接受空指针,反复调用也没有问题。这是标准规定的行为,那么有没有考虑过:

释放空指针代表本身逻辑已经错误了,因为这个指针是空,而让程序得以继续运行,反而会隐藏错误,在后续产生难以被解决和追溯的问题

关于这些争论,互联网上有很多,总而言之,盲目的遵守这些并不合适。

C++Guidelines 也都有一些关于 not_null 说法。


如果以上观点有待商榷的话,那么可以先回到规则本身的讲解,也就是这个示例,以及解释是很糟糕的。

class A {
   int* p = new int[10];
public:
   void dealloc() {
       delete[] p;
       p = nullptr;   // Good
   }
  ~A() { dealloc(); }
};

`R8.41` 中对内联函数的描述不正确

原文

是否对函数进行内联优化由实现定义,当函数执行的开销远大于调用的开销时,不会进行内联优化。

我能理解作者想要表达的意思是:“一个函数到底是否内联,最终由编译器自行决定”。

但是这个描述用词,显然是有问题的:“内联优化由实现定义”,“实现定义”这个词语,不应该用在这里,它通常不是用来表达这种意思的。

cppreference

由实现定义的行为(implementation-defined behavior)——程序的行为随实现而变动,遵从标准的实现必须为每个这样的行为的效果提供文档。例如 std::size_t 的类型或每个字节的位数,或者 std::bad_alloc::what 的文本。由实现定义的行为的一个子集是本地环境特定行为(locale-specific behavior),它取决于实现所提供的本地环境

可以参考微软文档的说法:

内联代码替换操作由编译器自行决定。 例如,如果某个函数的地址已被占用或编译器判定函数过大,则编译器不会内联该函数。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.