210 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			C
		
	
	
	
			
		
		
	
	
			210 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			C
		
	
	
	
| // SPDX-License-Identifier: GPL-2.0
 | |
| /*
 | |
|  * Taken from the kernel self tests, which in turn were based on
 | |
|  * a Syzkaller reproducer.
 | |
|  *
 | |
|  * Self test author and close_range author:
 | |
|  * Christian Brauner <christian.brauner@ubuntu.com>
 | |
|  *
 | |
|  * LTP Author: Richard Palethorpe <rpalethorpe@suse.com>
 | |
|  * Copyright (c) 2021 SUSE LLC, other copyrights may apply.
 | |
|  */
 | |
| /*\
 | |
|  * [Description]
 | |
|  *
 | |
|  * We check that close_range()
 | |
|  *
 | |
|  * - closes FDs
 | |
|  * - UNSHARES some FDs before closing them
 | |
|  * - it sets CLOEXEC (in both cloned process and parent)
 | |
|  * - combination of CLOEXEC and UNSHARE.
 | |
|  *
 | |
|  * The final test is the actual bug reproducer. Note that we call
 | |
|  * clone directly to share the file table.
 | |
|  */
 | |
| 
 | |
| #include <stdlib.h>
 | |
| 
 | |
| #include "tst_test.h"
 | |
| #include "tst_clone.h"
 | |
| 
 | |
| #include "lapi/clone.h"
 | |
| #include "lapi/close_range.h"
 | |
| 
 | |
| static int fd[3];
 | |
| 
 | |
| static inline void do_close_range(unsigned int fd, unsigned int max_fd,
 | |
| 				  unsigned int flags)
 | |
| {
 | |
| 	int ret = close_range(fd, max_fd, flags);
 | |
| 
 | |
| 	if (!ret)
 | |
| 		return;
 | |
| 
 | |
| 	if (errno == EINVAL) {
 | |
| 		if (flags & CLOSE_RANGE_UNSHARE)
 | |
| 			tst_brk(TCONF | TERRNO, "No CLOSE_RANGE_UNSHARE");
 | |
| 		if (flags & CLOSE_RANGE_CLOEXEC)
 | |
| 			tst_brk(TCONF | TERRNO, "No CLOSE_RANGE_CLOEXEC");
 | |
| 	}
 | |
| 
 | |
| 	tst_brk(TBROK | TERRNO, "close_range(%d, %d, %d)", fd, max_fd, flags);
 | |
| }
 | |
| 
 | |
| static void setup(void)
 | |
| {
 | |
| 	struct rlimit nfd;
 | |
| 
 | |
| 	SAFE_GETRLIMIT(RLIMIT_NOFILE, &nfd);
 | |
| 
 | |
| 	if (nfd.rlim_max < 1000) {
 | |
| 		tst_brk(TCONF, "NOFILE limit max too low: %lu < 1000",
 | |
| 			nfd.rlim_max);
 | |
| 	}
 | |
| 
 | |
| 	nfd.rlim_cur = nfd.rlim_max;
 | |
| 	SAFE_SETRLIMIT(RLIMIT_NOFILE, &nfd);
 | |
| }
 | |
| 
 | |
| static void check_cloexec(int i, int expected)
 | |
| {
 | |
| 	int present = SAFE_FCNTL(fd[i], F_GETFD) & FD_CLOEXEC;
 | |
| 
 | |
| 	if (expected && !present)
 | |
| 		tst_res(TFAIL, "fd[%d] flags do not contain FD_CLOEXEC", i);
 | |
| 
 | |
| 	if (!expected && present)
 | |
| 		tst_res(TFAIL, "fd[%d] flags contain FD_CLOEXEC", i);
 | |
| }
 | |
| 
 | |
| static void check_closed(int min)
 | |
| {
 | |
| 	int i;
 | |
| 
 | |
| 	for (i = min; i < 3; i++) {
 | |
| 		if (fcntl(fd[i], F_GETFD) > -1)
 | |
| 			tst_res(TFAIL, "fd[%d] is still open", i);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void child(unsigned int n)
 | |
| {
 | |
| 	switch (n) {
 | |
| 	case 0:
 | |
| 		SAFE_DUP2(fd[1], fd[2]);
 | |
| 		do_close_range(3, ~0U, 0);
 | |
| 		check_closed(0);
 | |
| 		break;
 | |
| 	case 1:
 | |
| 		SAFE_DUP2(fd[1], fd[2]);
 | |
| 		do_close_range(3, ~0U, CLOSE_RANGE_UNSHARE);
 | |
| 		check_closed(0);
 | |
| 		break;
 | |
| 	case 2:
 | |
| 		do_close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
 | |
| 		check_cloexec(0, 1);
 | |
| 		check_cloexec(1, 1);
 | |
| 
 | |
| 		SAFE_DUP2(fd[1], fd[2]);
 | |
| 		check_cloexec(2, 0);
 | |
| 		break;
 | |
| 	case 3:
 | |
| 		do_close_range(3, ~0U,
 | |
| 			       CLOSE_RANGE_CLOEXEC | CLOSE_RANGE_UNSHARE);
 | |
| 		check_cloexec(0, 1);
 | |
| 		check_cloexec(1, 1);
 | |
| 
 | |
| 		SAFE_DUP2(fd[1], fd[2]);
 | |
| 		check_cloexec(2, 0);
 | |
| 		break;
 | |
| 	}
 | |
| 
 | |
| 	exit(0);
 | |
| }
 | |
| 
 | |
| static void run(unsigned int n)
 | |
| {
 | |
| 	const struct tst_clone_args args = {
 | |
| 		.flags = CLONE_FILES,
 | |
| 		.exit_signal = SIGCHLD,
 | |
| 	};
 | |
| 
 | |
| 	switch (n) {
 | |
| 	case 0:
 | |
| 		tst_res(TINFO, "Plain close range");
 | |
| 		do_close_range(3, ~0U, 0);
 | |
| 		break;
 | |
| 	case 1:
 | |
| 		tst_res(TINFO, "Set UNSHARE and close range");
 | |
| 		do_close_range(3, ~0U, CLOSE_RANGE_UNSHARE);
 | |
| 		break;
 | |
| 	case 2:
 | |
| 		tst_res(TINFO, "Set CLOEXEC on range");
 | |
| 		do_close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
 | |
| 		break;
 | |
| 	case 3:
 | |
| 		tst_res(TINFO, "Set UNSHARE and CLOEXEC on range");
 | |
| 		do_close_range(3, ~0U,
 | |
| 			       CLOSE_RANGE_CLOEXEC | CLOSE_RANGE_UNSHARE);
 | |
| 		break;
 | |
| 	}
 | |
| 
 | |
| 	fd[0] = SAFE_OPEN("mnt/tmpfile", O_RDWR | O_CREAT, 0644);
 | |
| 	fd[1] = SAFE_DUP2(fd[0], 1000);
 | |
| 	fd[2] = 42;
 | |
| 
 | |
| 	if (!SAFE_CLONE(&args))
 | |
| 		child(n);
 | |
| 
 | |
| 	tst_reap_children();
 | |
| 
 | |
| 	switch (n) {
 | |
| 	case 0:
 | |
| 		check_closed(0);
 | |
| 		break;
 | |
| 	case 1:
 | |
| 		check_cloexec(0, 0);
 | |
| 		check_cloexec(1, 0);
 | |
| 		check_cloexec(2, 0);
 | |
| 		break;
 | |
| 	case 2:
 | |
| 		check_cloexec(0, 1);
 | |
| 		check_cloexec(1, 1);
 | |
| 		check_cloexec(2, 0);
 | |
| 		break;
 | |
| 	case 3:
 | |
| 		check_cloexec(0, 0);
 | |
| 		check_cloexec(1, 0);
 | |
| 		check_closed(2);
 | |
| 		break;
 | |
| 	}
 | |
| 
 | |
| 	do_close_range(3, ~0U, 0);
 | |
| 	check_closed(0);
 | |
| 
 | |
| 	if (tst_taint_check())
 | |
| 		tst_res(TFAIL, "Kernel tainted");
 | |
| 	else
 | |
| 		tst_res(TPASS, "No kernel taints");
 | |
| }
 | |
| 
 | |
| static struct tst_test test = {
 | |
| 	.tcnt = 4,
 | |
| 	.forks_child = 1,
 | |
| 	.mount_device = 1,
 | |
| 	.mntpoint = "mnt",
 | |
| 	.all_filesystems = 1,
 | |
| 	.needs_root = 1,
 | |
| 	.test = run,
 | |
| 	.caps = (struct tst_cap []) {
 | |
| 		TST_CAP(TST_CAP_DROP, CAP_SYS_ADMIN),
 | |
| 		{}
 | |
| 	},
 | |
| 	.taint_check = TST_TAINT_W | TST_TAINT_D,
 | |
| 	.setup = setup,
 | |
| 	.tags = (const struct tst_tag[]) {
 | |
| 		{"linux-git", "fec8a6a69103"},
 | |
| 		{},
 | |
| 	},
 | |
| };
 |