Upstream
latest kernel: 4.13-rc2
latest stable: 4.12.3
Event
无
文章来源
https://lwn.net
文章摘要
- The trouble with SMC-R
原文链接
关于 SMC-R, 读者可以自行阅读 rfc7609 或者参考 4.11 内核的相关实现。 本质上通过内核层面标准的 socket 接口来实现 tcp/rdma 在传输层层面的融合,并实现自动的软件 failover 功能(不考虑 RoCE lag 之类的 hw/fw 实现)。
主要特点:
socket 语义的兼容性, 兼容 SSL
应用不用关心底层传输层协议以及切换
几乎 bypass 协议栈,但是保留 socket 层
无法很好的支持 zero copy
软件 failover 协议定义完整,可以跨硬件(不同厂商的硬件),可以支持多网卡 (这点 RoCE lag 是不行的)
IBM 已经产品化
协议标准化
注:我认为不能 zero copy 是个很大的问题,兼容 socket 语义直接在 userspace 层面也是可以做的,可以全面的 bypass 协议栈,当然 SMC-R 的实现也不是一定在内核态,协议可以全部实现在用户态,SMC-R 对于RDMA 的实现对于兼容 socket 以及 软件 failover 层面的定义不错。但未必是基于性能最大化的考虑,从已经产品化和标准化的角度,总体来说是有参考意义的。
我个人不认为这本质上是一个技术问题,主要是 linux-rdma 阵营和 netdev 阵营的协调性的问题,文章中也提到了 这个 patch 被 merge 进入 4.11 根本没有经过 linux-rdma 相关邮件列表以及 maintainer 的首肯,D.M. 直接就给合并了,导致一定的争论,netdev 的邮件列表,rdma 的相关开发人员也没有特别关注到这个,总之是步调不太一致了。对于我们来说,我们不管他们各自出于什么目的,知道什么是你需要的,各自的优缺点更重要,把它们根据自己的业务需要融合到自己的产品里,而不是是否该进 mainline ,很多好的 patch 也未必都是被社区接受的,还有些 committer 根本不关心这个。
- Containers as kernel objects
原文链接
近年来内核对容器的支持越来越全面,催生了一批容器化的系统。有趣的是内核关于容器是什么样的并没有明确定义,它只是提供了一些用户态可以使用的机制。David Howells最近着力于改变这种情况,他提交了一组patchset把container定义成一个内核对象。然而社区不是很接受这样的修改。
容器是一种轻量化的虚拟化技术,为容器内的进程制造拥有整个系统的假象。通过区分namespace,为每个namespace分别提供网络、文件系统和cgroup等来隔离容器,控制资源使用。辅以安全模块或者seccomp来对容器更多限制。结果以可以接受的复杂程度提供了大量灵活的机制,这是非常Linux的方式。但是容器源于的缺失使得内核端的事情变得有些复杂。
增加容器object
Howell的patch引入了一套系统调用,用于维护容器。
int
container_create
(
const
char
*
name
,
...);
这个系统调用用于创建一个容器,容器名称由第一个参数指定。flags参数指定namespace相关,例如指定 CONTAINER_NEW_USER_NS创建的容器会使用心得namespace。返回值是用于引用创建容器的文件描述符。还有其他flags用于控制文件描述符关闭是否需要销毁容器这样的事情。
容器创建之后没有任何进程运行;如果用新挂载的namespace创建,容器里也不会有文件系统。fsopen()和fsmount()系统调用可以用来把文件系统添加到容器内部。“at”版本的文件系统调用(openat(),例如)可以接受容器的文件描述符。也可以用这样的方式让容器使用socket:
int
container_socket
(
int
container_fd
,...);
可以让容器使用netlink socket更方便。
让进程在容器里面运行的系统调用是:
pid_t
fork_into_container
(
int
container_fd
);
Howell认为还有其他可以加到这个机制的方法,例如suspend和restart容器的系统调用、容器cgroup管理的方法。只是Howell的修改前景并不明朗。
容器object抽象的不好?
很多人对这样的修改并不买账。
Jessica Frazelle认为Howell提出的容器object抽象的不够好,有其他很多创建容器的方法。还提到Open Containers Initiative中的很多runtime规范。
James Bottomley更直接的认为这种修复的方向不对。他认为现在容器好就好在大家不必就容器是什么样的达成一致。可以根据需要创建一个用户态namespace没有mount namespace,或者一个体系结构模拟容器只有一个mount namespace。 Kubernetes系统里面对容器的使用允许namespace在“pods”之间共享,这个跟当前的容器object的定义也是冲突的。
Eric Biederman的反对更强烈。namespace这样的修改会导致容器的所用conner cases完全暴露给用户态,开发者需要解决的问题更复杂,产生更多bug。
Upcalls
退一步来看,这套patchset的出发点也不是让用户态管理容器更加方便,而是希望使内核“upcall”在容器环境里面更好的运转。
通常来说,内核是系统的最底层。然而也不全是这样,例如 call_usermodehelper()调用创建一个用户态进程来完成某些工作——换句话说“向上调用”到用户态。用到这样的“upcall”的有:
*
core
-
dump
代码需要用户态进程来处理
dump
出的数据。
*
NFSv4
client
调用程序做
DNS
解析。
*
模块
loader
需要
helper
进程做模块的
demand
-
loading
*
内核秘钥管理代码也需要用户态
helper
这些upcall现在通常用到容器,然而内核没有容器的明确定义,并不能确定到底在那个容器里执行upcall,这样upcall可能会引起问题。
定义容器的确是解决这个问题的一个方法。但是Howell的解法引入了另外两个问题:(1)容器对象是解决问题的最好方法吗?(2)即便容器object有意义,需要暴露给用户态吗?
Colin Walters提出的另外一个解决方法是完全取缔“upcall”,用类似设备相关的events upcall被替换的方法。但是Jeff Layton指出这中解决方法只适用于部分问题,在其他情况下则可能引起系统可靠性的问题。
本文写在相关讨论火热进行的时候,发展难以预料。但是近期来看暴露到用户态的容器object难以进入内核。upcall的问题也许需要从其他方向进行解决,不过看起来这个问题还是需要一些时间。
3.Specifying the kernel ABI
原文链接
内核ABI规范化
在Open Source Summit Japan上,Sasha Levin谈了谈内核ABI的规范化,并介绍到目前为止取得的一些进展,当然还有大量工作尚未完成。主流观点认为,内核新增patch不应该破坏内核ABI兼容性,比如,一个运行在4.0内核上的应用程序,也需要能在更新的5.0内核上运行。但遗憾的是,当前还没有工具检测ABI的兼容性是否被破坏。于是乎,只能依靠用户去发现这种行为,然后报告到社区,由内核开发者修复。
由于没有ABI规范,一些基础软件,如glibc, qemu, strace, 为了保证可靠性,在系统调用前,往往进行重复的参数检查;这在性能方面带来一定损耗。同时,内核在系统调用路径上操作参数时,也容易破坏ABI的兼容性。更棘手的是,新发布了一个内核版本,一段时间后,用户发现一些老程序不能在新内核上运行(ABI不兼容);于此同时,另外一些用户基于新的ABI接口开发了新的应用。这时,内核开发者将面临一个两难的决策,不管如何,总会让一些用户不开心。
倘若ABI不能向后兼容,还会带来许多其它噩梦,不一一详述。因此,内核开发者需要一种内嵌在内核代码中的“规范”,用于描述哪些修改内核的行为是被禁止的。该规范一石二鸟,即强制要求内核和用户程序行为规范化,又解决了向后兼容问题。然而,该“规范”到底长什么样子呢?是人类可读的文档,还是机器可读的代码?从内核角度来看,需要能根据该规范生成代码,用于系统调用参数和返回值检查(这是ABI兼容性的一部分)。从用户态来看,它需要使应用程序和库访问内核ABI更容易,更有保障。
目前,最难的是如何确定规范的格式。open() 和 close() 系统调用很容易描述,但是许多其它系统调用更复杂,且相互耦合。因此,spec文档需要更详细的记录各个系统调用的行为,要比现有的man帮助文档内容更丰富,更偏向于实现原理。其次,这些spec文档需要经过严格的测试,以保证它没有破坏现有用户态程序的行为。
最后,Levin说自己正与syzkaller的开发者合作,参与一些前期工作, 希望对ABI规范化有所帮助。
- Namespaced file capabilities
原文链接
内核文件capabilities当前在用户名字空间(user namespaces)上的使用有很多的缺陷。当前主要的问题集中在可执行文件的capabilities是全局的。当前有内核开发人员提交了一组补丁 尝试让用户名字空间能够感知到capabilities,但是这组补丁也引发了关于这一机制工作方式的讨论。讨论的核心问题是文件capabilities如何指定给一组文件。
Linux Capabilities机制允许将一组特权授权一个进程,以便更细粒度的控制特权用户的权限,从而限制传统Unix中root用户的特权带来的安全问题。举个例子,一个非特权程序需要发送信号给另外一个不相关的进程时,只需要具备CAP_KILL权限,而并不需要获得root权限。
传统Unix系统,特权操作通过setuid(1)赋予普通用户。而在带有capabilities的系统上,只需要将可执行文件与对应权限关联即可。而上面提到的文件capabilities则在2.6.24内核上就已经进入到内核主线。
用户名字空间允许一组进程在名字空间中以root用户运行,而其实这个用户在名字空间外的root ID会映射到一个普通ID上来执行操作。为了执行一些特权操作,这个名字空间中root用户将需要执行的一些程序通过setuid设置特权操作。而这些设置了特权操作的程序在名字空间外则不再具备特权。同样的功能目前的文件capabilities则不具备。因为上面提到的全局视角问题。所有用户名字空间对某个可执行文件都具有相同的视角。同时因为名字空间中的进程实际上并不是运行在root名字空间,因此也无法修改文件capabilities。
当前文件capabilities是通过扩展属性(Extended Attribute, EA)来实现的,内部实际上是将这些文件capabilities保存在security.capability属性中。内核会对security.*进行特殊处理。只有特权程序(比如:具备CAPSYSADMIN能力)才被允许修改这些属性。这也就解释了为什么容器内非特权程序是无法添加文件capabilities的原因。同时也可以看到,当前没有方法能够针对特定的用户名字空间来保存不同的EA。
Stefan Berger提交的补丁中通过扩展EA的语法来尝试解决上述问题。该方法会将一个root名字空间下的用户ID映射到特定用户空间上的UID 0。比如一个UID 1000的用户启动一个用户名字空间并作为root用户运行,则他可以访问UID 1000的所有文件。如果该用户在用户名字空间中尝试添加capabilities,则这个信息会被保存成
security
.
capability@uid
=
1000
在名字空间外,这个新属性没有任何效果。而在用户名字空间中,这个属性则显示为security.capability,因此容器中的文件可以按照授予的特权来运行。
当前的补丁并不是针对所有EA,而是涉及安全的一部分,比如security.capability以及security.selinux。当然随后security.selinux的特性被移除了,因为SELinux维护者Stephen Smalley指出当前的实现有问题。
Casey Schaufler反对这一补丁,原因是如果两个用户空间使用相同的UID,并且共享目录树,那么这些文件capabilities在两个名字空间中都是可见的。他认为使用UID作为关键字来映射文件capabilities是不正确的。他认为应该找个其他的持久ID与用户名字空间做关联,以解决上面的问题。
James Bottomley反对这一补丁的理由是在动态分配用户ID的容器上无法工作。他建议创建一个@uid前缀来解决动态分配的问题。
总之,虽然原始补丁问题多多,但是在最近几个版本我们就会看到支持用户名字空间的文件capabilities特性了。
5.Zero-copy networking
原文链接
网络大部分时候都是性能敏感的,受数据拷贝操作的影响比较大,网络数据包的zero-copy一直在不断改进。通过sendfile()系统调用,文件内容可以不用拷贝到用户态就直接发送出去;但是这个只能用于发送文件数据,类似对数据排序后的输出这种的没法使用zero-copy的sendfile()。 来自google的MSGZEROCOPY系列就是为了解决上述问题。首先需要在socket建立之后调用setsockopt()设置新的SOCKZEROCOPY选项;然后可以用如下方式实现一个zero-copy的发送:
status
=
send
(
socket
,
buffer
,
length
,
MSG_ZEROCOPY
);
都成功的情况下,给定的buffer将会锁定在内存里面。在send返回之前,由于zero-copy,注意需要保护好buffer不被修改。 zero-copy机制实现里面会将通知消息发送到和socket绑定的error queue里面,这样可以获知到数据发送完和buffer何时可以被重新使用。通知消息通过如下方式读取:
status
=
recvmsg
(
socket
,
&
message
,
MSG_ERRORQUEUE
);
使用zero-copy传输需要将页锁定到内存,对数据量很小的传输这个锁定内存操作开销比较大,因此并不推荐对小数据量的传输使用该方式。实际上即便设置了MSG_ZEROCOPY,内核里面也有可能对小操作使用拷贝的方式,但在这种情况,会有额外的状态数据包的开销。 有些情况zero-copy是不可能的,比如:网络设备不支持生成checksum,这个时候内核需要自己计算,也就不可避免的要拷贝数据;另外类似需要对数据进行加密发送,这种情况也是无法实现zero-copy。
benchmark(netperf)测试的结果显示zero-copy有39%的性能提升,当然在实际环境并无法达到这么好效果,一个线上负载测试的结果显示有5-8%的性能提升。 参考论文
- Hardened usercopy whitelisting
原文链接
更强的用户拷贝白名单化
有很多种方法尝试去搞垮操作系统内核。有一种特别有效的方法,如果可以做到的话,就是攻击用户空间和内核空间之间拷贝数据的操作。如果内核可以被欺骗拷贝到用户空间大量数据,就会造成信息泄漏。反方向,如果攻击者覆盖内核内存会变得更糟。目前,内核已经有一系列抵抗攻击的patch,但仍然有些工作需要汇合进去。
内核里用到的堆内存主要来自于slab分配器。 hardened usercopy patch set(已经merge到4.8内核),试图去限制错误拷贝的影响,它主要是通过保证单个的拷贝操作不会跨越一个slab分配好的对象和它的下一个对象的边界。但是内核 会从slab分配器里获得大量的内存对象,并且它没有必要在用户和内核空间拷贝整个对象。假如只需要拷贝对象的部分的话,那么阻止一个无赖的拷贝操作(从那些没有必要暴露的结构里拷贝或者是拷贝进去)将会有用。
举个例子,mmstruct结构描述了一个进程的虚拟地址空间。它饮食了大量的安全敏感信息。其中一个字段为savedauxy字段被拷贝到用户空间或者到从用户空间拷贝。用于操作这个字段的prctl()函数并不会直接把它拷贝到mmstruct结构里。有一些晦涩的代码(在ELF二进制代码)传递这个字段到copyto_user()。这样限制拷贝操作就不会有暴露整个结构的风险。
授权保护是hardened usercopy whitelisting的目标。经验说法是我们需要知道这些patch的由来。这些代码是来自于grsecurity/Pax 补丁集。
简单来讲,这些补丁集增强了hardened usercopy whitelisting机器,主要通过一个slab分配好的对象上的用户拷贝区域。只有在这个区域里的数据才可以被拷贝到用户空间(通过copytouser()或者copyfromuser())。值得强调的是,对于原来的一些操作比如put_user(),no checking机制已经被应用上。这样这些操作的大小就是固定的,不会再受到攻击的影响。
正常情况下,一个slab缓存使用kmemcachecreate()分配,这个补丁加入一个新的函数
struct
kmem_cache
*
kmem_cache_create_usercopy
(
const
char
*
name
,...);
参数useroffset和usersize是新的参数。它们描述了从slab缓存中分配对象的的区域。如果usersize为0,就不允许拷贝。从kmemcachecreate()创建的slab和这些函数比如kmalloc()的获取的对象,都是白名单。
不管什么时候,从slab获取的对象被传递到用户空间拷贝函数里。那些要被拷贝的区域将会被检查以保证它们全在白名单里。如果检查失败,内核oops就会发生。
上述设计的一个影响就是那些对象只有单个的区域可以被暴露到用户空间。如果有拷贝不止一个字段的需求,那么这些字段必须合在一起,这样单个的区域才可以覆盖到它们。为了达到这个目的,在白名单化阶段就需要把一些结构重新组织。目前,在补丁集里,许多结构已经被专门白名单化了。
补丁集里的最后一步就是为内存分配创建一个新的标志,GFPUSERCOPY。有一些专门的系统调用来强制内核从用户空间以可控制的大小分配结构。正常来讲,这是没有坏处的,只要大小控制在合理边界即可。但是一些攻击也会根据这个特点来实施。如果那些分配带上了GFPUSERCOPY标志,它们将会从一个分开的slab上获取,这样导致控制堆区域的布局很难。
不太清楚这些补丁什么时候进主线,但目前看,进入主线没有一些严重的障碍。