弃用的接口、语言特性、属性和约定¶
在理想情况下,可以在一个开发周期内将所有弃用 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);
相反,应使用分配器的双因子形式:
foo = kmalloc_array(count, size, GFP_KERNEL);
具体来说,kmalloc()
可以替换为 kmalloc_array()
,kzalloc()
可以替换为 kcalloc()
。
如果没有双因子形式可用,则应使用溢出饱和辅助函数:
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()
函数通常是正确的替代方案,但请注意,这些函数要求字符串以空字符或换行符终止。
strcpy()¶
strcpy()
不对目标缓冲区执行边界检查。这可能导致超出缓冲区末尾的线性溢出,从而引发各种异常行为。尽管 CONFIG_FORTIFY_SOURCE=y 和各种编译器标志有助于降低使用此函数的风险,但没有充分的理由新增此函数的使用。安全的替代方案是 strscpy()
,但必须注意任何使用了 strcpy()
返回值的情况,因为 strscpy()
不返回指向目标缓冲区的指针,而是返回已复制的非空字节数(或截断时的负 errno 值)。
对空终止字符串使用 strncpy()¶
使用 strncpy()
并不能保证目标缓冲区会以空字符终止。由于缺少终止符,这可能导致各种线性读溢出和其他异常行为。如果源内容短于目标缓冲区大小,它还会用空字符填充目标缓冲区,这对于仅使用空终止字符串的调用者来说可能是不必要的性能损失。
当目标需要空终止时,替代方案是 strscpy()
,但必须注意任何使用了 strncpy()
返回值的情况,因为 strscpy()
不返回指向目标缓冲区的指针,而是返回已复制的非空字节数(或截断时的负 errno 值)。任何仍需要空字符填充的情况应改用 strscpy_pad()
。
如果调用方使用非空终止字符串,则应使用 strtomem()
,并且目标应标记为 __nonstring 属性以避免未来的编译器警告。对于仍需要空字符填充的情况,可以使用 strtomem_pad()
。
strlcpy()¶
strlcpy() 会首先读取整个源缓冲区(因为其返回值应与 strlen()
匹配)。这种读取可能会超出目标大小限制。这既低效,如果源字符串不是空终止的,还可能导致线性读溢出。安全的替代方案是 strscpy()
,但必须注意任何使用了 strlcpy() 返回值的情况,因为 strscpy()
在截断时将返回负 errno 值。
%p 格式说明符¶
传统上,在格式字符串中使用“%p”会导致 dmesg、proc、sysfs 等中常见的地址暴露缺陷。为了避免这些缺陷被利用,内核中所有“%p”的使用都被打印为哈希值,使其无法用于寻址。不应在内核中新增“%p”的使用。对于文本地址,使用“%pS”可能更好,因为它会生成更有用的符号名称。对于几乎所有其他情况,根本不要添加“%p”。
转述 Linus 当前的指导意见:
如果哈希化的“%p”值毫无意义,请问自己指针本身是否重要。也许它应该被完全移除?
如果您真的认为真实的指针值很重要,那么为什么某些系统状态或用户权限级别被认为是“特殊”的?如果您认为自己可以充分证明其合理性(在注释和提交日志中),足以经受住 Linus 的审查,那么您可以使用“%px”,并确保拥有合理的权限。
如果您在调试过程中遇到“%p”哈希导致问题,可以暂时使用调试标志“no_hash_pointers”启动。
变长数组(VLAs)¶
使用栈 VLA 会产生比静态大小栈数组差得多的机器码。尽管这些非平凡的性能问题足以成为消除 VLA 的理由,但它们也存在安全风险。栈数组的动态增长可能超出栈段中的剩余内存。这可能导致崩溃,可能覆盖栈末尾的敏感内容(当构建时没有 CONFIG_THREAD_INFO_IN_TASK=y),或覆盖栈附近内存(当构建时没有 CONFIG_VMAP_STACK=y)。
隐式 switch case 穿透¶
C 语言允许在 case 语句末尾缺少“break”语句时,switch case 会穿透到下一个 case。然而,这在代码中引入了歧义,因为不总是清楚缺失的 break 是有意为之还是一个 bug。例如,仅从代码来看,不明显 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() 进行脆弱的大小计算(需要减去单个尾随元素的大小才能获得“头部”的正确大小)。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
结果为 零
,而人们可能以为它代表最近为尾随数组 items
分配的动态内存的总字节大小。以下是此问题的几个示例:链接 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);
};
};