已弃用的接口、语言特性、属性和约定¶
在一个理想的世界里,有可能在单个开发周期中将所有已弃用的 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)。
strncpy() 在 NUL 终止的字符串上¶
使用 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” 临时引导。
可变长度数组 (VLA)¶
使用堆栈 VLA 会产生比静态大小的堆栈数组差得多的机器代码。虽然这些非平凡的性能问题足以成为消除 VLA 的理由,但它们也是一种安全风险。堆栈数组的动态增长可能会超出堆栈段中的剩余内存。这可能会导致崩溃,可能会覆盖堆栈末端的敏感内容(当构建时未使用 CONFIG_THREAD_INFO_IN_TASK=y 时),或者覆盖堆栈相邻的内存(当构建时未使用 CONFIG_VMAP_STACK=y 时)。
隐式 switch case 穿透¶
当 case 的末尾缺少“break”语句时,C 语言允许 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 [表达式];
零长度和单元素数组¶
内核中经常需要提供一种方法来声明结构中具有动态大小的尾部元素集合。内核代码应始终使用“灵活数组成员”来处理这些情况。不应再使用旧式的单元素或零长度数组。
在较旧的 C 代码中,动态大小的尾部元素是通过在结构末尾指定一个单元素数组来完成的
struct something {
size_t count;
struct foo items[1];
};
这导致了通过 sizeof() 进行的脆弱的大小计算(这将需要删除单个尾部元素的大小以获得 “header” 的正确大小)。引入了GNU C 扩展以允许零长度数组,以避免这些类型的大小问题
struct something {
size_t count;
struct foo items[0];
};
但这导致了其他问题,并且没有解决两种样式共享的一些问题,例如无法检测到此类数组何时意外地没有在结构的末尾使用(这可能直接发生,或者当这样的结构位于联合、结构体等中时)。
C99 引入了“灵活数组成员”,它完全没有数组声明的数值大小
struct something {
size_t count;
struct foo items[];
};
这是内核期望声明动态大小的尾部元素的方式。当灵活数组不是结构中的最后一个时,它允许编译器生成错误,这有助于防止一些未定义的行为 bug 被无意中引入到代码库中。它还允许编译器正确分析数组大小(通过 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
结果为 zero
,而人们可能认为它表示最近为尾部数组 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);
};
};