一次奇怪的bug---对sysrq-trigger的虚拟化

本文探讨了在虚拟化环境中,通过向/proc/sysrq-trigger写入'i'来杀死除init进程外的所有进程的问题。作者详细分析了内核源码,并提出了解决方案,即在内核层面进行虚拟化处理,使特定进程被视为init进程。

背景:我们的系统做了虚拟化处理,系统存在多个init,每个init负责一个业务分支

问题的产生:在某次操作的时候,上层应用中的一个进程会调用KillAllProcesses()接口,向节点/proc/sysrq-trigger写i,目的是kill掉当前系统中除了init进程外的其他子进程。

static void KillAllProcesses() { android::base::WriteStringToFile("i", "/proc/sysrq-trigger"); }

本来这是一次正常的操作,但是发现每次操作之后,子init负责的整个用户态就挂了,这和我的预期很不一致,我的目标是kill掉除了init进程外的其他子进程,当时百思不得其解,因为就是一个向/proc/sysrq-trigger 写i的动作,为什么会杀掉我的"init"呢,我翻了好几遍有关/proc/sysrq-trigger的用法,用法没有问题,这个问题当时僵持了有一段时间,最后和组内一个同事交流(多交流,一个人看问题有时候很容易陷入死结,别人可以看到你看不到的地方)发现这样的现象是对的,只是我忽略了一点,我将该进程的创造始祖"init"当做了一个真正的init

     因为虚拟化的原因,其实这个"init"在内核视角并不是一个真正的init,所以向/proc/sysrq-trigger写i后,/proc/sysrq-trigger做了它该做的,杀死了除了init之外的其他进程,而我的"init"就属于这个其他进程。

    问题找到了了,那么思路就很清晰了,我需要让系统认为我的"init"也是一个init,当识别到我的init后,不要去kill掉,将他当做一个正常的init,只去kill掉它的所有子孙进程即可,所以我们需要在内核态对sysrq-trigger做虚拟化操作,让他认识我的"init".

    /proc/sysrq-trigger节点在内核drivers/tty/sysrq.c文件中生成,顺藤摸瓜,在sysrq_key_table数组中找到了关于写"i"操作的结构体:

static struct sysrq_key_op *sysrq_key_table[36] = {
	&sysrq_loglevel_op,		/* 0 */
	&sysrq_loglevel_op,		/* 1 */
	&sysrq_loglevel_op,		/* 2 */
	&sysrq_loglevel_op,		/* 3 */
	&sysrq_loglevel_op,		/* 4 */
	&sysrq_loglevel_op,		/* 5 */
	&sysrq_loglevel_op,		/* 6 */
	&sysrq_loglevel_op,		/* 7 */
	&sysrq_loglevel_op,		/* 8 */
	&sysrq_loglevel_op,		/* 9 */

	/*
	 * a: Don't use for system provided sysrqs, it is handled specially on
	 * sparc and will never arrive.
	 */
	NULL,				/* a */
	&sysrq_reboot_op,		/* b */
	&sysrq_crash_op,		/* c & ibm_emac driver debug */
	&sysrq_showlocks_op,		/* d */
	&sysrq_term_op,			/* e */
	&sysrq_moom_op,			/* f */
	/* g: May be registered for the kernel debugger */
	NULL,				/* g */
	NULL,				/* h - reserved for help */
	&sysrq_kill_op,			/* i */

进而找到sysrq_kill_op

static struct sysrq_key_op sysrq_kill_op = {
	.handler	= sysrq_handle_kill,
	.help_msg	= "kill-all-tasks(i)",
	.action_msg	= "Kill All Tasks",
	.enable_mask	= SYSRQ_ENABLE_SIGNAL,
};

看到hander这个名字就八九不离十了,看看,一直往下跟:

static void sysrq_handle_kill(int key)
{
	send_sig_all(SIGKILL);
	console_loglevel = CONSOLE_LOGLEVEL_DEBUG;
}

/*
 * Signal sysrq helper function.  Sends a signal to all user processes.
 */
static void send_sig_all(int sig)
{
	struct task_struct *p;

	read_lock(&tasklist_lock);
	for_each_process(p) {
		if (p->flags & PF_KTHREAD)
			continue;
		if (is_global_init(p))
			continue;

		do_send_sig_info(sig, SEND_SIG_FORCED, p, true);
	}
	read_unlock(&tasklist_lock);
}

上述代码可以看到,for_each_process会遍历所有的用户态进程,但是他有一个动作:

if (is_global_init(p))这里就是判断该进程是否为init进程的地方,写i操作为什么不会杀死init的原因就在这里。

所以我只需要在这里做处理就可以了

下面是改进后的代码:

static void send_sig_all(int sig)
{
	struct task_struct *p;
	struct task_struct *child_reaper;
	struct pid_namespace *ns;
	struct nsproxy *nsproxy;
	struct list_head *pos;

	read_lock(&tasklist_lock);
	nsproxy = task_nsproxy(current);
	if(nsproxy) {
		ns = nsproxy->pid_ns_for_children;

		if(!ns) {
			pr_err("get the ns fail in sysrq.c\n");
			return;
		}
	} else {
		pr_err("get the nsproxy fail in sysrq.c\n");
		return;
	}
    if(ns == &init_pid_ns) {     //这里是真正的init
		for_each_process(p) {
			if (p->flags & PF_KTHREAD)
				continue;
			if (is_global_init(p))
				continue;

			do_send_sig_info(sig, SEND_SIG_FORCED, p, true);
		}
	} else {			//child:All processes belonging to namespce except init in the     
          namespace should be killed
		child_reaper = ns->child_reaper;
		if(!child_reaper) {
			pr_err("get the child_reaper fail in sysrq.c\n");
			return;
		}
		list_for_each(pos,&child_reaper->children) {
			p = list_entry(pos,struct task_struct,sibling);
			if (p->flags & PF_KTHREAD)
				continue;
			if (child_reaper->tgid == p->tgid)
				continue;
			do_send_sig_info(sig, SEND_SIG_FORCED, p, true);
		}
	}
	read_unlock(&tasklist_lock);
}

上述代码基本的思路和关键是要获得当前进程所属的pid namespace,还有当前命名空间的init进程

nsproxy = task_nsproxy(current);

ns = nsproxy->pid_ns_for_children;

上面两句用于获取当前进程的命名空间

child_reaper = ns->child_reaper;

child_reaper 的定义为:

struct pid_namespace {
	struct kref kref;
	struct pidmap pidmap[PIDMAP_ENTRIES];
	struct rcu_head rcu;
	int last_pid;
	unsigned int nr_hashed;
	struct task_struct *child_reaper;

child_reaper指向的是当前命名空间的init进程,每个命名空间都有一个作用相当于全局init进程的进程。

能够找到当前进程的命名空间和当前命名空间的init进程,这个事就好办了,然后遍历该进程,杀掉除了该进程外的其他子孙进程

ok,问题解决。

题外话:

  该问题解决后,因为兄弟组有部署docker,docker也使用了namespace,所有我们想docker会不会也有这样的问题,如果有的话,岂不是很危险,我们做了实验,在docker上向sysrq-trigger写i,发现这个节点我们写不了,?这么神奇,最后发现docker做了规避,docker将这个sysrq-trigger节点做了处理,将 该节点重新挂载成为一个只读的节点,所以你是无法对其写的,只能读读读,果然优秀的软件考虑的真是到位啊。。。。。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值