废弃的接口、语言特性、属性和约定 【ChatGPT】

发布时间 2023-12-08 23:00:31作者: 摩斯电码

废弃的接口、语言特性、属性和约定

在一个完美的世界中,将所有废弃的 API 实例转换为新的 API 并在单个开发周期内完全移除旧的 API 是可能的。然而,由于内核的规模、维护层次结构和时间安排,这种转换并不总是可行的。这意味着在移除旧 API 的同时,新的实例可能会悄悄地进入内核,从而增加了移除 API 的工作量。为了向开发人员介绍已被废弃的内容及其原因,创建了这个列表,以便在内核中提出使用废弃内容的情况时进行指引。

__deprecated

虽然这个属性在视觉上标记了一个接口为废弃,但它不再在构建过程中产生警告,因为内核的一个持久目标是在没有警告的情况下构建,而且没有人实际上在做任何事情来移除这些废弃的接口。虽然使用 __deprecated 可以在头文件中注明旧的 API,但这并不是完整的解决方案。这样的接口必须要么完全从内核中移除,要么添加到这个文件中,以阻止其他人在将来使用它们。

BUG() 和 BUG_ON()

应该使用 WARN() 和 WARN_ON(),并尽可能优雅地处理“不可能”的错误条件。虽然 BUG()-系列的 API 最初设计用于作为“不可能的情况”断言,并“安全地”终止内核线程,但事实证明它们实际上太过危险。(例如:“锁需要以什么顺序释放?各种状态是否已经恢复?”)非常普遍的情况是,使用 BUG() 会使系统不稳定或完全崩溃,这使得调试或获得可行的崩溃报告变得不可能。Linus 对此有非常强烈的感受。

请注意,WARN()-系列应该只用于“预期不可达”的情况。如果要警告“可达但不希望出现”的情况,请使用 pr_warn()-系列函数。系统所有者可能已经设置了 panic_on_warn sysctl,以确保他们的系统在面对“不可达”条件时不会继续运行。(例如,参见像这样的提交。)

分配器参数中的开放式算术运算

不应在内存分配器(或类似)函数参数中执行动态大小计算(特别是乘法),因为存在溢出的风险。这可能导致值环绕并且进行了比调用者期望的更小的分配。使用这些分配可能导致堆内存的线性溢出和其他不良行为。(这种情况的一个例外是字面值,编译器可以在可能溢出时发出警告。然而,在这些情况下,首选的方法是按照下面建议的方式重构代码。)

例如,不要将 count * size 作为参数使用,如下所示:

foo = kmalloc(count * size, GFP_KERNEL);

而应该使用分配器的 2 因子形式:

foo = kmalloc_array(count, size, GFP_KERNEL);

具体来说,kmalloc() 可以被替换为 kmalloc_array(),而 kzalloc() 可以被替换为 kcalloc()。

如果没有 2 因子形式可用,应该使用饱和溢出辅助函数:

bar = dma_alloc_coherent(dev, array_size(count, size), &dma, GFP_KERNEL);

另一个常见的情况是计算具有其他结构体数组的尾随数组的大小,如下所示:

header = kzalloc(sizeof(*header) + count * sizeof(*header->item),
                 GFP_KERNEL);

而应该使用辅助函数:

header = kzalloc(struct_size(header, item, count), GFP_KERNEL);

注意
如果在包含零长度或一个元素数组的结构体上使用 struct_size(),请重构这样的数组用法,并改用柔性数组成员。

对于其他计算,请组合使用 size_mul()、size_add() 和 size_sub() 辅助函数。例如,在这种情况下:

foo = krealloc(current_size + chunk_size * (count - 3), GFP_KERNEL);

应该使用辅助函数:

foo = krealloc(size_add(current_size,
                        size_mul(chunk_size,
                                 size_sub(count, 3))), GFP_KERNEL);

更多细节,请参阅 array3_size() 和 flex_array_size(),以及相关的 check_mul_overflow()、check_add_overflow()、check_sub_overflow() 和 check_shl_overflow() 函数系列。

simple_strtol()、simple_strtoll()、simple_strtoul()、simple_strtoull()

simple_strtol()、simple_strtoll()、simple_strtoul() 和 simple_strtoull() 函数明确忽略溢出,这可能导致调用者出现意外结果。相应的 kstrtol()、kstrtoll()、kstrtoul() 和 kstrtoull() 函数往往是正确的替代方案,尽管需要注意这些函数要求字符串以 NUL 或换行符结尾。

strcpy()

strcpy() 在目标缓冲区上不执行边界检查。这可能导致超出缓冲区末尾的线性溢出,导致各种不良行为。虽然 CONFIG_FORTIFY_SOURCE=y 和各种编译器标志有助于减少使用此函数的风险,但没有理由添加新的使用此函数的情况。安全的替代方案是 strscpy(),尽管必须注意任何使用 strcpy() 返回值的情况,因为 strscpy() 不返回指向目标的指针,而是返回复制的非 NUL 字节的计数(或在截断时返回负的 errno)。

对 NUL 结尾字符串使用 strncpy()

使用 strncpy() 不能保证目标缓冲区将被 NUL 结尾。这可能导致各种线性读取溢出和其他不良行为,因为缺少终止符。如果源内容比目标缓冲区大小短,它还会在目标缓冲区上填充 NUL,这可能对只使用 NUL 结尾字符串的调用者来说是不必要的性能损失。

当需要目标缓冲区以 NUL 结尾时,替代方案是 strscpy(),尽管必须注意任何使用 strncpy() 返回值的情况,因为 strscpy() 不返回指向目标的指针,而是返回复制的非 NUL 字节的计数(或在截断时返回负的 errno)。仍然需要 NUL 填充的情况应该使用 strscpy_pad()。

如果调用者使用非 NUL 结尾字符串,应该使用 strtomem(),并且目标应该用 __nonstring 属性标记,以避免未来的编译器警告。仍然需要 NUL 填充的情况可以使用 strtomem_pad()。

strlcpy()

strlcpy() 首先读取整个源缓冲区(因为返回值意味着与 strlen() 相匹配)。这种读取可能超出目标大小限制。这既效率低下,又可能导致源字符串没有 NUL 结尾时的线性读取溢出。安全的替代方案是 strscpy(),尽管必须注意任何使用 strlcpy() 返回值的情况,因为 strscpy() 在截断时会返回负的 errno 值。

%p 格式说明符

传统上,在格式字符串中使用“%p”会导致 dmesg、proc、sysfs 等中的常规地址暴露漏洞。为了避免这些漏洞被利用,内核中所有“%p”的使用都被打印为散列值,使其无法用于寻址。不应该在内核中添加新的“%p”使用。对于文本地址,使用“%pS”可能更好,因为它会产生更有用的符号名称。对于几乎所有其他情况,根本不要添加“%p”。

引用 Linus 目前的指导:

  • 如果散列的“%p”值是毫无意义的,问问自己指针本身是否重要。也许应该完全删除它?

  • 如果你真的认为真实的指针值很重要,为什么某些系统状态或用户特权级别被认为是“特殊的”?如果你认为你可以证明(在注释和提交日志中)足够好以经得起 Linus 的审查,也许你可以使用“%px”,并确保你有合理的权限。

如果你正在调试某些东西,其中“%p”散列导致问题,你可以临时使用调试标志“no_hash_pointers”来启动系统。

可变长度数组(VLAs)

使用栈上的可变长度数组会产生比静态大小的栈数组更糟糕的机器代码。尽管这些非平凡的性能问题足以理由消除可变长度数组,但它们也是一种安全风险。栈数组的动态增长可能超过栈段中剩余的内存。这可能导致崩溃,在没有启用CONFIG_THREAD_INFO_IN_TASK=y的情况下,可能会覆盖栈末尾的敏感内容,或者在没有启用CONFIG_VMAP_STACK=y的情况下覆盖与栈相邻的内存。

隐式的switch case穿透

C语言允许在case的末尾缺少"break"语句时,switch case穿透到下一个case。然而,这会在代码中引入歧义,因为并不总是清楚缺少的break是有意为之还是一个错误。例如,仅仅从代码中看不出STATE_ONE是否有意设计为穿透到STATE_TWO:

switch (value) {
case STATE_ONE:
        do_something();
case STATE_TWO:
        do_other();
        break;
default:
        WARN("unknown state");
}

由于由于缺少"break"语句而导致的一系列缺陷,我们不再允许隐式的穿透。为了识别有意的穿透情况,我们采用了一个伪关键字宏"fallthrough",它会展开为gcc的扩展__attribute__((__fallthrough__))。(当C17/C18 [[fallthrough]]语法被C编译器、静态分析器和IDE更常见地支持时,我们可以切换到使用该语法作为伪关键字的宏。)

所有的switch/case块必须以以下之一结束:

  • break;

  • fallthrough;

  • continue;

  • goto <label>;

  • return [expression];

零长度和单元素数组

在内核中,经常需要提供一种声明具有动态大小的结构体尾部元素集合的方法。内核代码应始终使用"柔性数组成员"来处理这些情况。不再使用旧的一元素或零长度数组的风格。

在旧的C代码中,通过在结构体末尾指定一个一元素数组来实现动态大小的尾部元素:

struct something {
        size_t count;
        struct foo items[1];
};

这导致了通过sizeof()进行脆弱的大小计算(需要从大小为1的尾部元素中减去大小以获得"头部"的正确大小)。引入了GNU C扩展以允许零长度数组,以避免这些大小问题:

struct something {
        size_t count;
        struct foo items[0];
};

但这导致了其他问题,并且没有解决两种风格共同存在的一些问题,比如无法检测到这样的数组意外地被用于结构体的非末尾位置(可能直接发生,或者当这样的结构体位于联合体、结构体的结构体等中时)。

C99引入了"柔性数组成员",在数组声明中完全没有数值大小:

struct something {
        size_t count;
        struct foo items[];
};

这是内核期望声明动态大小尾部元素的方式。它允许编译器在柔性数组不出现在结构体末尾时生成错误,这有助于防止一些未定义行为的错误被无意中引入到代码库中。它还允许编译器正确分析数组大小(通过sizeof()、CONFIG_FORTIFY_SOURCE和CONFIG_UBSAN_BOUNDS)。例如,没有机制警告我们对零长度数组应用sizeof()运算符总是得到零的情况:

struct something {
        size_t count;
        struct foo items[0];
};

struct something *instance;

instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;

size = sizeof(instance->items) * instance->count;
memcpy(instance->items, source, size);

在上面的代码的最后一行,size的结果是零,当人们可能认为它代表了最近为尾部数组项分配的动态内存的总大小(以字节为单位)。这里有几个这个问题的例子:链接1,链接2。相反,柔性数组成员具有不完整类型,因此不能应用sizeof()运算符,因此任何对这种运算符的误用将在构建时立即被注意到。

对于一元素数组,必须明确知道这样的数组占用的空间至少与单个对象的空间一样多,因此它们会增加封装结构体的大小。每当人们想要计算包含此类数组作为成员的结构体的动态内存的总大小时,这很容易出错:

struct something {
        size_t count;
        struct foo items[1];
};

struct something *instance;

instance = kmalloc(struct_size(instance, items, count - 1), GFP_KERNEL);
instance->count = count;

size = sizeof(instance->items) * instance->count;
memcpy(instance->items, source, size);

在上面的示例中,我们必须记住在使用struct_size()辅助函数时计算count - 1,否则我们将无意中为多分配一个items对象的内存。实现这一点最干净、最不容易出错的方法是使用柔性数组成员,结合struct_size()和flex_array_size()辅助函数:

struct something {
        size_t count;
        struct foo items[];
};

struct something *instance;

instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;

memcpy(instance->items, source, flex_array_size(instance, items, instance->count));

在两种特殊情况下,需要使用DECLARE_FLEX_ARRAY()辅助函数进行替换。(注意,在UAPI头文件中,它被命名为__DECLARE_FLEX_ARRAY()。)这些情况是当柔性数组要么是结构体中唯一的元素,要么是联合体的一部分时。这些情况在C99规范中是不允许的,但没有技术原因(可以通过这些地方已经使用这样的数组和DECLARE_FLEX_ARRAY()使用的解决方法来看出)。例如,要将以下代码转换为:

struct something {
        ...
        union {
                struct type1 one[0];
                struct type2 two[0];
        };
};

必须使用辅助函数:

struct something {
        ...
        union {
                DECLARE_FLEX_ARRAY(struct type1, one);
                DECLARE_FLEX_ARRAY(struct type2, two);
        };
};