为什么 getrandom() 调用无法返回?

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 问题背景

负责在 Linux 4.9 内核上开发 Qt 应用的同事反馈了一个问题:将 Qt 库由一个低版本迁移到更高版本后,准备进入测试阶段,于是将 Qt 程序由平时开发时的手工启动,变更为开机自启动,但是开机后发现 Qt 程序无法启动!奇怪的是,用 kill 命令停止 Qt 程序后,等待一段时间后(系统引导完成后,等待一段时间),再手动启动 Qt 程序,程序大多数时候可以运行起来,但仍然有一定的几率无法启动。

3. 问题分析

首先,使用 gdb attach 定位了程序卡在了 glibcgetrandom()调用上:

main(int argc, char *argv[])
	QApplication app(argc, argv)
		...
		getrandom(..., ..., 0)

接下来, 通过 glibc 源码了解到 getrandom() 仅是对系统调用 sys_getrandom() 的简单调用,不存在任何可能进入无限循环的代码,这引导我们将目光投向了内核侧。在这一点上,我们也通过

cat /proc/<pid>/stack # 需开启内核配置项 CONFIG_STACKTRACE
cat /proc/<pid>/syscall # 需开启内核配置项 CONFIG_HAVE_ARCH_TRACEHOOK
cat /proc/<pid>/wchan # 需开启内核配置项 CONFIG_KALLSYMS

进行了进一步的确认,发现程序确实在内核的 wait_for_random_bytes() 函数内打转,而且 top 也观察到程序大多时候处于 S 态(即睡眠状态),这意味着程序在等待某个事件的发生。如果内核打开了 ftrace ,也可以通过 ftrace 来观察程序内核侧的调用,会比前面的方式观察得更加清楚。

让我们来看一下系统调用 sys_getrandom()为什么会卡住,或者说为什么它一直无法返回用户空间:

sys_getrandom()    /* 场景中的调用 flags == 0, 所以不用考虑 flags != 0 的代码路径 */
	...
	/*
	 * 如果 crng 没有准备好,需要进行等待。
	 * 在我们的问题场景,程序就是卡在这里一直无法返回。
	 */
	if (!crng_ready()) { /* (likely(crng_init > 1)) */
		wait_for_random_bytes()
			wait_event_interruptible(crng_init_wait, crng_ready()) /* 进程陷入睡眠 */
	}

从上面的代码看出,导致程序无法返回用户空间的原因,是因为 crng_ready() 不成立。进一步分析内核代码,我们能够看到如下这些场景,可能使 crng_ready() 成立继而唤醒睡眠进程:

/* 
 * crng 随机数熵池数据来源 (1):用户随机输入事件。 
 */
input_event()
	input_handle_event()
		add_input_randomness()
			add_timer_randomness(&input_timer_state, ...)
				r = &input_pool;
				...
				credit_entropy_bits(r, ...)
					if (r == &input_pool) {
						int entropy_bits = entropy_count >> ENTROPY_SHIFT;
						
						if (crng_init < 2 && entropy_bits >= 128) {
							crng_reseed(&primary_crng, r);
								...
								if (crng == &primary_crng && crng_init < 2) {
									...
									crng_init = 2;
									...
									/* 唤醒等待随机数的进程 */
									wake_up_interruptible(&crng_init_wait);
									...
								}
							...
						}
					}               

/* 
 * crng 随机数熵池数据来源 (2):磁盘操作。
 */
blk_update_bidi_request()
	if (blk_queue_add_random(rq->q))
		add_disk_randomness(rq->rq_disk)
			add_timer_randomness(disk->random, 0x100 + disk_devt(disk));
				...
				crng_reseed(&primary_crng, r) /* 后面流程同上 */ 

/* 
 * crng 随机数熵池数据来源 (3):中断。
 */
handle_irq_event_percpu()
	add_interrupt_randomness()
		...
		crng_reseed(&primary_crng, r) /* 后面流程同上 */

/* 
 * crng 随机数熵池数据来源 (4):随机数硬件驱动。 
 */
add_hwgenerator_randomness()
	...
	crng_reseed(&primary_crng, r) /* 后面流程同上 */

/*
 * 更多的 crng 随机数熵池数据来源
 */
...... 

CRNG(Cryptographic Random Number Generator) 随机数子系统的基本工作原理,是利用随机的事件,如用户输入、磁盘操作、中断等,向随机数熵池填充数据,而用户则从熵池中拿取数据,从而使得随机数真正变得随机。而导致问题场景中,开机启动 Qt 程序时,卡在 sys_getrandom() ,是因为嵌入式系统中,外设少,产生的对随机数熵池做贡献的事件少,导致熵池数据不足,从而卡在内核侧;而手工启动程序时,一般系统都运行了一段时间,随机数熵池被各种事件填充了足够的数据,所以 Qt 程序可以启动起来,偶尔的不能启动,也是因为 Qt 程序,在随机数熵池尚未填充足够数据时运行。另外,低版本的 Qt 能够运行起来,是因为它在 getrandom() 是采用非阻塞的方式进行调用。

4. 问题修复

找到了问题的根因,接下来我们要做的事情,自然是对问题进行修复。一开始的修复方案,我们的思路是,既然问题是由于 CRNG 随机数熵池数据不足导致的,而且系统又提供接口,允许特权用户对该熵池进行填充,那么我们就主动的填充数据,来解决问题。后来经过一番 Google 搜索,发现已经有现成的方案 haveged ,我们就直接拿来用了。haveged 的基本工作原理就是对 /dev/random 发起 ioctl(RNDADDENTROPY)。 关于 haveged 的使用,可参考其手册 https://man.archlinux.org/man/extra/haveged/haveged.8.en

当然,haveged 并不是唯一的修正方案,事实上,社区针对该问题进行了一系列的讨论,Linus 本人在 Linux 5.4 合入了针对该问题内核侧的一个解决方案:
https://lwn.net/Articles/802360/
https://lore.kernel.org/lkml/CAHk-=wgjC01UaoV35PZvGPnrQ812SRGPoV7Xp63BBFxAsJjvrg@mail.gmail.com/

  • 19
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值