Sockaddr 结构体 续篇
Source: Hacker News
Background
Linux Kernel Self‑Protection Project(KSPP)在完成十年工作后,其众多目标之一是确保所有数组引用都能够进行边界检查,即使是大小在编译时未知的弹性数组成员。内核中一个特别棘手的弹性数组成员甚至根本没有被声明为弹性数组。大约一年前,LWN 对网络子系统中被广泛使用的 sockaddr 结构的安全性提升工作进行了报道。一年后,Kees Cook 仍在寻找将这项工作收尾的方法。
The Problem with struct sockaddr
传统上,struct sockaddr 被定义为:
struct sockaddr {
short sa_family;
char sa_data[14];
};
sa_data 字段在 1980 年代初首次为 BSD Unix 定义该结构时足够容纳网络地址,但现在已经不够用了。因此,内核和用户空间的大量代码会传递指向实际更大结构的 struct sockaddr 指针,这些更大的结构为它们需要保存的地址提供了更多空间。换句话说,sa_data 正被当作弹性数组成员使用,尽管它并未被声明为弹性数组。
struct sockaddr 的普遍使用给更好地检查结构体中数组成员的使用带来了阻碍。
在去年的最后一次讨论中,内核的大部分代码已经改为使用 struct sockaddr_storage(实现为 struct __kernel_sockaddr_storage),其数据数组足够大,能够容纳任何已知的网络地址。曾尝试将 struct sockaddr 的定义改为显式的弹性数组成员,但该工作遇到了障碍:
- 内核中许多地方将
struct sockaddr嵌入到另一个结构体中。 - 在大多数情况下,
sa_data并未 被当作弹性数组成员使用。 - 开发者随意地在包含结构体的任意位置嵌入
struct sockaddr,往往并不在结构体末尾。
如果将 sa_data 重定义为弹性数组成员,编译器将不再知道该结构体的实际大小。因此,它无法布局包含 struct sockaddr 的结构体,导致内核构建时出现成千上万的警告。内核开发者宁愿面对数组溢出的风险,也不愿看到大量警告,于是工作陷入停滞。
Proposed Solution: struct sockaddr_unsized
一种可能的解决方案是用 struct sockaddr_storage 替换嵌入的 struct sockaddr 字段,从而消除弹性数组成员。但这会让包含结构体的大小不必要地膨胀,因而不受欢迎。
相反,Cook 正在开发一组补丁,引入另一种 sockaddr 变体:
struct sockaddr_unsized {
__kernel_sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[]; /* flexible address data */
};
它的目的是在内部网络子系统接口中使用,在这些接口中 sa_data 的大小需要是弹性的,但实际大小也是已知的。例如,struct proto_ops 中的 bind() 方法被定义为:
int (*bind) (struct socket *sock,
struct sockaddr *myaddr,
int sockaddr_len);
myaddr 的类型可以改为 struct sockaddr_unsized *,因为 sockaddr_len 提供了 sa_data 数组的真实大小。Cook 的补丁系列做了许多此类替换,消除了网络子系统中可变大小的 sockaddr 结构的使用。完成后,就不再有任何代码会读取超出 14 字节 sa_data 数组的 struct sockaddr。于是,struct sockaddr 可以恢复为其经典的、非弹性定义,并且可以对使用该结构的代码进行数组边界检查。
Outlook
此更改足以让所有这些警告消失,许多人将其视为一个好的收尾点。然而,仍然存在大量 sockaddr_unsized 结构,它们仍然是潜在的灾难性溢出来源。等尘埃落定后,注意力可能会转向为这些结构实现边界检查。
补丁集中提到的一种可能做法是最终添加一个 sa_data_len 字段,使结构体能够包含其 sa_data 数组的长度。这样就可以使用 counted_by() 注解 来记录字段之间的关系,帮助编译器插入边界检查。
虽然用 Rust 编写新代码的能力有望减少内核中的内存安全缺陷,但事实是内核中仍有大量 C 代码短时间内不会消失。任何能够让这些代码更安全的措施都值得欢迎。struct sockaddr 的各种变体看似荒唐,却是为一个已有四十多年历史的 API 引入一点安全性的过程的一部分。十年的 KSPP 让内核更安全了,但工作远未完成。