Really fixing getrandom()
5.3 kernel开发周期最后几天的时候有一场很热烈的讨论,关于getrandom() API以及需要revert一个ext4优化patch避免其导致系统启动过程中随机事件太少。长久来说,我们肯定不可能因为文件系统优化得太好了而阻止其合入kernel,所以大家都达成共识后面需要找到一个更好的解决方案。大家都想不到什么更好的方案,因此我们其实很惊奇的看到后来5.4里面提出的处理方案很少有人反对。
5.3中问题的核心:getrandom() API是一个阻塞调用,它会等到系统搜集到足够多的随机事件初始化好随机数生成器之后,才能让调用此API的函数返回。这样的行为受到很多批评,辩护的人反而不多。不过修改getrandom()不是件容易的事情。改得不小心的话,都会导致它返回的“随机数”被预测到,从而在安全场景下产生风险。所以,尽管有多种方案提出来,大家看起来这些方案估计要几年才能扯清楚。
还有一种方案来确保getrandom()在boostrap过程中不要阻塞:尽快搜集足够多的随机事件从而确保随机数生成器在有人要用的时候早已经初始化好了。具体来说有好几种做法,如果CPU本身有一个硬件随机数生成器的话,直接用硬件方案,有人可能觉得这个方案不可信,不过大多数人都认为把硬件事件和其他事件混在一起是一个安全可靠的做法,哪怕硬件随机数生成器本身不太随机。不过,不是所有的CPU都拥有硬件随机数生成器的,因此这个方法不够通用。还有一个其他方案就是让bootloader或者user space最早期执行的代码来初始化random pool(随机数池),这个方法有些情况下是可行的,不过也不是通用方案。
另一个可能方案是利用jitter entropy:从天生具有随机特性的硬件上搜集随机事件。哪怕一系列很简单的指令的执行时序(timing)也会每次都不相同,这主要是因为有多级缓存(cache)、预测执行、系统上执行的其他任务等共同干扰导致的。针对jitter entropy在过去多年的研究表明它是一个可行方案,虽然无法证明这是一个truly nondeterministic(真正不确定特性),不过肯定是无法预测的(unpredictable),采用jitter entropy生成的数据可以经过各种随机数测试的考验。
在5.3版本发布不久,Thomas Gleixner建议利用一种简单的jitter-entropy机制来初始化随机熵池。Linus Torvalds认为这个方案“不是非常可靠”,不过他觉得这个方向还是很有可取之处的。他自己稍微改变了一下这个方案,经过简短讨论之后,在5.4 merge window末期合入了。
Torvalds的patch增加了一个新函数try_to_generate_entropy()。如果有人在随机数池还没完全准备好的时候就要求获取随机数的话,就会导致调用这个函数。这个函数非常简单,开头是这样的:
[size=12] static void try_to_generate_entropy(void)
{
struct {
unsigned long now;
struct timer_list timer;
} stack;[/size]
首先在stack(栈)里声明了一个timer,在kernel里面很少有这种用法,不过这里是故意放在stack上的。如果timer函数在另一个CPU上运行的话,在访问此函数的stack空间时就会产生cache contention(cache竞争),从而引起耗时(timing)变化。
[size=12] stack.now = random_get_entropy();
/* Slow counter - or none. Don't even bother */
if (stack.now == random_get_entropy())
return;
timer_setup_on_stack(&stack.timer, entropy_timer, 0);[/size]
在大多数架构上,random_get_entropy()仅仅会直接读出timestamp counter(TSC)。TSC在每个clock cycle会增加一次,这样两次连续调用一定会产生两个不同的数值。
不过具体来说这两个数字的差值是不固定的,没法预测出来。上面的代码只是想验证一下kernel正在运行的这个系统确实是有一个高频TSC的。否则的话这个算法没法生效了。型号大多数现代硬件都有TSC。在TSC检查通过之后,timer就可以拿来用了:
[size=12] while (!crng_ready()) {
if (!timer_pending(&stack.timer))
mod_timer(&stack.timer, jiffies+1);
mix_pool_bytes(&input_pool, &stack.now, sizeof(stack.now));
schedule();
stack.now = random_get_entropy();
}[/size]
这里的循环就是jitter-entropy算法的核心了。上面声明的timer,被配置成在很短的时间之后就会到期(下一个jiffy值,一般是0ms到10ms之间的值),到期触发之后的中断可能会在任何一个CPU上。这样timer到期实际上就是让执行的指令流更加复杂化(希望能增加随机性)。
每走一遍这个循环,都会查询一下TSC,把它的值混如随机数池中。timer到期之后就会重新配置进行下一轮。这样会导致scheduler也参与进来,引入其他各种各样的复杂代码,增加返回前代码执行的不可预知性。这个循环会在随机数池初始化好之后才停下。因为一个jiffy的时长内会发生很多事情,因此这个循环预计会执行很多次,能把多个TSC读数混进随机数池内去。
[size=12] del_timer_sync(&stack.timer);
destroy_timer_on_stack(&stack.timer);
mix_pool_bytes(&input_pool, &stack.now, sizeof(stack.now));
}[/size]
循环之后就是清理代码,会删掉timer,并且再塞一点进随机数池。
timer函数本身是这样实现的:
[size=12] static void entropy_timer(struct timer_list *t)
{
credit_entropy_bits(&input_pool, 1);
}[/size]
换句话说,每次timer到期的时候,都会让随机数池里面增加一个bit的随机熵。随机数池只要有128 bit的随机熵就算是准备好了。这样可能jitter循环会执行最多128 jiffies,大致等同于100HZ tick frequency的系统上运行一秒钟的时间,这还是在假设系统其他部分一点都没有贡献随机熵的情况。这样可能还是会在启动过程中产生用户可以观察到的暂停一下的现象,不过比起完全阻塞住要好得太多了。
当Torvalds决定合入这些代码的时候,他认为这并不一定算是此问题的最终解决方案:“我不是说要让我的patch作为这个问题的结论。我个人觉得这个patch还行,不算太离谱。如果它让非常严格的人感到有点不爽,然后能发一个改进版的给我,那就算是值得了。”
因此他其实是沿用了以前的方式,尽早提交一个patch,寄希望于引起其他人的抱怨从而给出更好的方案。目前来说,还没有多少关于这个改动的评论。这其实有点奇怪,因为此前在讨论中可是有着非常多种观点的。除非有人能提供一个更好的主意,或者证明这个算法给出的随机熵其实是可以用某种方法预测出来的,否则这个方案就会一直沿用下去了。希望这个patch能达成目的,一方面避免getrandom()一直阻塞住,另一方面也能让kernel的随机数值保持真正不可以预测的特性。
作者:LWN
说明:文章遵循CC BY-SA 4.0许可协议。
By Jonathan Corbet
October 17, 2019
From: https://lwn.net/Articles/802360/
相关文章:LWN: 修复getrandom()
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/258106.html