LWN: 修复getrandom()

640
点击上方蓝色“ Linux News搬运工”关注我们~

Fixing getrandom()

By Jake Edge

内核5.3版本里面有人报出一个启动过程中死机问题,最终演化成了一个影响很大的、充满争议性的linux-kernel mailing list讨论话题。大概介绍一下这里的问题,因为近期有些改动导致ext4文件系统在boot阶段早期所做的I/O操作数量变少了,也就是说中断发生的次数变少了,与此同时Linux的getramdom() syscall设计的行为是在 /dev/urandom 初始化完成之前会一直阻塞住、不返回给user space调用者。而出问题的系统上,启动早期阶段没有搜集到足够的随机事件(主要是那些随机的中断event不够多),所以这种系统可能会一直保持死机状态。这个话题最后引起关于getramdon()的实现机制的争论。

Ahmed S. Darwish是最先报告出这个问题的人,当时他debug时一直追踪到GNOME Display Manager (GDM,用于处理图形界面的登录行为)。他发现GDM一直卡在调用getrandom()来试图生成“MIT magic cookie”(用在X Win系统上的认证领域)的状态。大家其实觉得这里为了满足X Windows security需求的话,其实不需要强壮的随机数,只要能用一个很弱的随机数生成器就能达到要求了,不会太过于损坏安全性。Darwish也说这里GDM只索取了少数几个byte(有5次调用,每次获取了16 byte)。不过正如Theodore Y. Ts'o所说,这些并不影响事情的本质,缺省来说getrandom()就是不会返还任何数据,直到CRNG(cryptographic random number generator)初始化好才可以,而这需要entropy(系统熵值高,通常是要采集到足够多的随机事件之后)。

Darwish也做过二分法调查,确认是一个ext4的commit导致的,它优化了boot阶段的disk I/O操作次数。而不幸的是在Darwish的电脑上这也导致系统的entropy熵值变少,所以系统无法完成启动了。这个ext4改动暂时先被revert了。

getrandom()

2014年的时候,getrandom()被加入kernel,当时一个理由是因为LibreSSL project在Linux上抱怨在文件描述符被黑客恶意用光的情况下没有办法生成随机数。此前的获取方法是从/dev/urandom来读取,不过如果攻击者让所有文件都被打开,那么就没法打开/dev/urandom了,也就没法拿到随机苏。这样大家就创建了getrandom()系统调用,能够在不需要文件描述符(甚至像chroom或者container里这种没有/dev/urandom文件节点)的情况下就能拿到随机数了。getrandom()是故意设计成这样的,就是希望在/dev/urandom随机数池初始化好之前阻塞住不返回。在发明getrandom()调用之前,user space没有任何方法能确保系统采集了足够的entropy熵值并生成了随机数池。也就是说/dev/urandom的行为是kernel ABI定义好的,不可以更改,而新加的系统调用则可以不遵循这个限制。

要想完成CRNG的初始化,需要有512 bit的entropy,或者说需要有4096次中断。Ts'o觉得这个值选得有些保守。getrandom()的文档里清楚的描述了这个阻塞行为,不过user space开发者仍然有人会错用这个API。Ts'o认为今后这里肯定会有问题,必须得找个方法,要么能让user space开发者知道这里不能保证启动阶段之后就能使用getrandom,要么让大家能绝对信任Intel和RDRAND指令。否则的话,今后kernel里面哪里又有一个优化导致中断数变少的话,又会导致某人的系统或者application卡死了。

Linus Torvalds指出,RDRAND指令又不是在每个系统上都有的,所以没法依靠RDRAND指令来解决所有问题。他也觉得今后ssd会使用更多的polling而不是中断的情况下,情况会变得更糟,大家要针对这种“更少中断”的环境早作打算。

Error return

Ts'o建议这个API可以加一个flag,让用户程序在调用getrandom()的时候能选择是否只阻塞比如说2分钟,时间到了之后就尽最大努力给一个随机数返回值即可。不过Torvalds觉得这里的阻塞操作根本就不应该出现。他认为要加flag的话也是针对阻塞行的,而缺省行为也是马上返回,这样开发user space application的开发者直觉就跟真实行为吻合了。或者干脆返回一个错误值而不是阻塞在那里,也就是说返回一个尽量随机的数字,不过同时返回一个-EINVAL,因为调用这个API调用的太早了。

有不少人赞同这种做法,Darwish发出了一个RFC patch就是按照这个方向实现的。Alexander E. Patrakov帮忙修改了一下commit message描述信息,在其中也在抱怨说这种做法是把责任转嫁给user space了。Ts'o明确表示这个方案不好,不过他还是贡献了一些代码来让kernel在编译时(或者通过kernel boot时参数)可以指定是否使用阻塞行为。Darwish之后又修改了一次,加了更多的commit message内容,详细介绍了问题的背景细节。之后,应Torvalds的要求,把-EINVAL的返回值也去掉了。所以这样改了之后getrandom()的行为就跟用户此前直接读/dev/urandom的行为一样了:调用者每次调用都能拿到此时尽量随机的一个数值。

Lennart Poettering觉得这个方案真不好,就像是鸵鸟把头埋在沙子里一样,用户明明是要拿到一个随机数来生成对系统安全可能非常重要的秘钥,结果我们只返回一个不那么随机的随机数。他认为这个问题根本就不是kernel的问题,就该user space来改,应该让系统设计者提供一个可靠的硬件随机数生成器,如果系统上没有的话,那就别怪我们启动时卡死。

这里的一个问题是glibc加了getrandom()的wrapper的时候,也加了一个类似OpenBSD上的getentropy()函数。OpenBSD上的这个函数并不会阻塞,而glibc版本的getentropy()既然是基于getrandom()的,那么在启动阶段确实有可能会卡死住。user space开发者可能不会意识到getentropy()在这些系统上的不同行为,尽管文档里面明明都写清了。Torvalds以及其他一些人也都指出,如果系统真的被阻塞住在等entropy熵值达标的情况的话,大概率来说entropy永远无法达标。User space需要想办法创造一些随即行为(例如按键、访问磁盘)来生成足够中断数量,CRNG才能完成初始化。

Torvalds还指出,getrandom(0)的调用者其实有两类人,一类是需要阻塞住,一直等到CRNG初始化好拿到真正的随机数,另一类是仅仅想随便拿个随机数值,没有想太多。而glibc的getentropy()用户可能也会是第二类人。

Limiting delays

Torvalds也不喜欢加一个编译选项的主意。他第一选择是在CRNG没初始化时,限制getrandop()第一次调用的阻塞时间为15秒,也保证此后每次调用最多delay 30秒。这种情况返回值是-EAGAIN,这样用户程序就能识别这个现象。在代码的注释里,他说:“根本不应该强制阻塞住来等待一个随机数,kernel不应该满足他们这个需求。”

这里又引出了另外一番热烈讨论。Poettering觉得这个问题不该让kernel基于这种“user space不可靠的”想法来解决。Darwish则贴出了一个patch set,实现了getrandom2()系统调用,可以使用明确的flag来指定行为。不过Torvalds认为getrandom()的flag bit足够用了,不应该引入一个新的系统调用。

Torvalds建议还是完善这个flag的值,能更明确的指明行为。GRND_EXPLICIT flag可以用来供user space使用来表明“我知道我在干什么,我自己负责”。而GRND_SECURE和GRND_INSECURE值则分别用阻塞和不阻塞的行为实现,不过这两个都需要设置GRND_EXPLICIT bit才行。这种做法没有处理getrandom(0)这种情况,不过Torvalds也考虑好了:“针对getrandom(0)这种情况只好按照以前行为来处理,不过因为这个参数跟其他GRND_xxx参数已经区分开来了,我们就可以加些timeout,或者加个warning就好。而其他新加的GRND_xxx这些flag场景则不用给出warning信息,其中GRND_INSECURE可以返回伪随机数,但是不用给出warning信息,因为这个新加的模式,用户肯定知道INSECURE是不安全的。”

Ts'o问道,为什么不让getrandom()跟GRND_SECURE的行为一致就好了,这样就能跟它当前行为一模一样。Torvalds坚决拒绝了,他觉得Ts'o担心的只是一些理论上可行的攻击,而没有关注真正的getrandom(0)问题。Torvalds打算把这个GRND_xxx的支持patch backport到各个stable kernel上去,所以所有会改变getrandom(0)行为的patch都会是独立于上述修改的单独patch,成为mainline-only的patch。

Jitter entropy

Patrakov觉得可以跟haveged entropy daemon一样来利用jitter entropy。haveged在多个发行版里面都有使用,用来确保系统启动早期阶段能有足够的entropy。他也说这个方法争议颇大,因为有人担心这些数据不是真随机。Torvalds也说他就是比较担心这个,不过他认为这倒是一个当前这个烂摊子的一个可行解决方案,就是能告诉用户说系统不会卡太久,得益于jitter entropy,这个方案虽然不是最好的方案,不过理论上是可行的。

但是getrandom()在kernel已经支持了5年了,glibc也有超过2年了,所以大家看来这已经是kernel的ABI了。有些人想调用getrandom(0)达到的目的,不应该被偷偷改成提供一个伪随机数,这样可能会导致user space程序出错的。这是Andy Lutomirski的看法。他也觉得getrandom()设计的时候没有考虑周全,但是现在不应该再改了。他认为现有程序在调用getrandom(0)的时候可能就是需要真随机数的,就像openssl就会依赖getentropy()和getrandom来获取数据,kernel不应该改变这里的行为,否则就是大家信誓旦旦要避免的破坏ABI的行为了,现有的程序仍能正常运行但是会失去安全性。

Lutomirski认为这就是一个kernel bug,现在应该积极解决这个问题:“不如我们想办法在getrandom()里面做一些事情来增加系统熵值。”Torvalds比较赞同这一点,不过他更倾向于使用jitter-entropy来作为临时方案。

总之目标是要让调用者能意识到他们每种参数调用时的明确行为,其中有些flag可能会导致思索的。目前只有default的getrandom(0)定义还不清晰,所以Torvalds决心一定要找个方法能让系统不要死锁,例如通过timeout或者jitter-entropy等。

不过他还有点担心jitter entropy需要多长时间才能采集到足够的entropy信息。最多15秒还是可以接受的,不过最好能找一些加速的jitter-entropy方法。Patrakov说不需要加速,2秒钟就能搞定了。此外,Matthew Garrett也说Fuchsia系统里的Zircon kernel也是用jitter entropy来完成CRNG初始化的,所以这个技术还是比较可靠的。

ABI

值得注意的是,Torvalds在这个问题里面不太介意修改ABI,这跟他的通常观点不太一致。他的理由是,所有那些会导致getrandom(0)的行为改变成为超时的场景,都是一些理论场景,而我们面临的启动阶段死锁问题是真实存在的。这种理论场景就是说用户需要在非常空闲的系统上启动才会出现。Garrett认为这是getrandom(0)设计时所针对的场景,而Torvalds认为这种情况下生成秘钥就是一个假想情况而已。

Torvalds和Lutomirski的主要分歧在于,是否要在getrandom()里面提供一种路径来永远阻塞。Torvalds认为可以提供作为getrandom()的非缺省行为,而Lutomirski则认为应该简化getrandom(),彻底去除对/dev/random随机数池的依赖。如果真有用户依赖如今的getrandom(0)的行为的话,还是可以直接读/dev/random来达到的。

这个问题的讨论非常长,反转也很多。Torvalds的最终决定还不清楚是什么。看起来需要修改一下getrandom(0)行为,至于是用超时,还是jitter entropy机制,这还没有定下来。不过可以看出Torvalds已经决定抛弃那种永远阻塞的行为了。

同时也表明内核社区又一次在API/ABI设计上出了问题。Torvalds以及其他人都认为getrandom()的行为不应该是永远阻塞的,不过这个信息给出的时候晚了5年。API/ABI的review是kernel过去多年都想改善的,希望这次能给大家提个醒,今后能花更多时间来review和test这些ABI相关的改动,否则如何能承诺ABI永远不变呢?

全文完

LWN文章遵循CC BY-SA 4.0许可协议。

极度欢迎将文章分享到朋友圈 
热烈欢迎转载以及基于现有协议修改再创作~

长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~

640?wx_fmt=jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值