LSM: SafeSetID: add selftest
This patch adds a selftest for the SafeSetID LSM. The test requires mounting securityfs if it isn't mounted, creating test users in /etc/passwd, and configuring policies for the SafeSetID LSM through writes to securityfs. Signed-off-by: Micah Morton <mortonm@chromium.org> Signed-off-by: James Morris <james.morris@microsoft.com>
This commit is contained in:
		
							parent
							
								
									2181e084b2
								
							
						
					
					
						commit
						c67e8ec03f
					
				
							
								
								
									
										1
									
								
								tools/testing/selftests/safesetid/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tools/testing/selftests/safesetid/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| safesetid-test | ||||
							
								
								
									
										8
									
								
								tools/testing/selftests/safesetid/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								tools/testing/selftests/safesetid/Makefile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| # SPDX-License-Identifier: GPL-2.0
 | ||||
| # Makefile for mount selftests.
 | ||||
| CFLAGS = -Wall -lcap -O2 | ||||
| 
 | ||||
| TEST_PROGS := run_tests.sh | ||||
| TEST_GEN_FILES := safesetid-test | ||||
| 
 | ||||
| include ../lib.mk | ||||
							
								
								
									
										2
									
								
								tools/testing/selftests/safesetid/config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tools/testing/selftests/safesetid/config
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| CONFIG_SECURITY=y | ||||
| CONFIG_SECURITYFS=y | ||||
							
								
								
									
										334
									
								
								tools/testing/selftests/safesetid/safesetid-test.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										334
									
								
								tools/testing/selftests/safesetid/safesetid-test.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,334 @@ | ||||
| // SPDX-License-Identifier: GPL-2.0
 | ||||
| #define _GNU_SOURCE | ||||
| #include <stdio.h> | ||||
| #include <errno.h> | ||||
| #include <pwd.h> | ||||
| #include <string.h> | ||||
| #include <syscall.h> | ||||
| #include <sys/capability.h> | ||||
| #include <sys/types.h> | ||||
| #include <sys/mount.h> | ||||
| #include <sys/prctl.h> | ||||
| #include <sys/wait.h> | ||||
| #include <stdlib.h> | ||||
| #include <unistd.h> | ||||
| #include <fcntl.h> | ||||
| #include <stdbool.h> | ||||
| #include <stdarg.h> | ||||
| 
 | ||||
| #ifndef CLONE_NEWUSER | ||||
| # define CLONE_NEWUSER 0x10000000 | ||||
| #endif | ||||
| 
 | ||||
| #define ROOT_USER 0 | ||||
| #define RESTRICTED_PARENT 1 | ||||
| #define ALLOWED_CHILD1 2 | ||||
| #define ALLOWED_CHILD2 3 | ||||
| #define NO_POLICY_USER 4 | ||||
| 
 | ||||
| char* add_whitelist_policy_file = "/sys/kernel/security/safesetid/add_whitelist_policy"; | ||||
| 
 | ||||
| static void die(char *fmt, ...) | ||||
| { | ||||
| 	va_list ap; | ||||
| 	va_start(ap, fmt); | ||||
| 	vfprintf(stderr, fmt, ap); | ||||
| 	va_end(ap); | ||||
| 	exit(EXIT_FAILURE); | ||||
| } | ||||
| 
 | ||||
| static bool vmaybe_write_file(bool enoent_ok, char *filename, char *fmt, va_list ap) | ||||
| { | ||||
| 	char buf[4096]; | ||||
| 	int fd; | ||||
| 	ssize_t written; | ||||
| 	int buf_len; | ||||
| 
 | ||||
| 	buf_len = vsnprintf(buf, sizeof(buf), fmt, ap); | ||||
| 	if (buf_len < 0) { | ||||
| 		printf("vsnprintf failed: %s\n", | ||||
| 		    strerror(errno)); | ||||
| 		return false; | ||||
| 	} | ||||
| 	if (buf_len >= sizeof(buf)) { | ||||
| 		printf("vsnprintf output truncated\n"); | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	fd = open(filename, O_WRONLY); | ||||
| 	if (fd < 0) { | ||||
| 		if ((errno == ENOENT) && enoent_ok) | ||||
| 			return true; | ||||
| 		return false; | ||||
| 	} | ||||
| 	written = write(fd, buf, buf_len); | ||||
| 	if (written != buf_len) { | ||||
| 		if (written >= 0) { | ||||
| 			printf("short write to %s\n", filename); | ||||
| 			return false; | ||||
| 		} else { | ||||
| 			printf("write to %s failed: %s\n", | ||||
| 				filename, strerror(errno)); | ||||
| 			return false; | ||||
| 		} | ||||
| 	} | ||||
| 	if (close(fd) != 0) { | ||||
| 		printf("close of %s failed: %s\n", | ||||
| 			filename, strerror(errno)); | ||||
| 		return false; | ||||
| 	} | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| static bool write_file(char *filename, char *fmt, ...) | ||||
| { | ||||
| 	va_list ap; | ||||
| 	bool ret; | ||||
| 
 | ||||
| 	va_start(ap, fmt); | ||||
| 	ret = vmaybe_write_file(false, filename, fmt, ap); | ||||
| 	va_end(ap); | ||||
| 
 | ||||
| 	return ret; | ||||
| } | ||||
| 
 | ||||
| static void ensure_user_exists(uid_t uid) | ||||
| { | ||||
| 	struct passwd p; | ||||
| 
 | ||||
| 	FILE *fd; | ||||
| 	char name_str[10]; | ||||
| 
 | ||||
| 	if (getpwuid(uid) == NULL) { | ||||
| 		memset(&p,0x00,sizeof(p)); | ||||
| 		fd=fopen("/etc/passwd","a"); | ||||
| 		if (fd == NULL) | ||||
| 			die("couldn't open file\n"); | ||||
| 		if (fseek(fd, 0, SEEK_END)) | ||||
| 			die("couldn't fseek\n"); | ||||
| 		snprintf(name_str, 10, "%d", uid); | ||||
| 		p.pw_name=name_str; | ||||
| 		p.pw_uid=uid; | ||||
| 		p.pw_gecos="Test account"; | ||||
| 		p.pw_dir="/dev/null"; | ||||
| 		p.pw_shell="/bin/false"; | ||||
| 		int value = putpwent(&p,fd); | ||||
| 		if (value != 0) | ||||
| 			die("putpwent failed\n"); | ||||
| 		if (fclose(fd)) | ||||
| 			die("fclose failed\n"); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| static void ensure_securityfs_mounted(void) | ||||
| { | ||||
| 	int fd = open(add_whitelist_policy_file, O_WRONLY); | ||||
| 	if (fd < 0) { | ||||
| 		if (errno == ENOENT) { | ||||
| 			// Need to mount securityfs
 | ||||
| 			if (mount("securityfs", "/sys/kernel/security", | ||||
| 						"securityfs", 0, NULL) < 0) | ||||
| 				die("mounting securityfs failed\n"); | ||||
| 		} else { | ||||
| 			die("couldn't find securityfs for unknown reason\n"); | ||||
| 		} | ||||
| 	} else { | ||||
| 		if (close(fd) != 0) { | ||||
| 			die("close of %s failed: %s\n", | ||||
| 				add_whitelist_policy_file, strerror(errno)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| static void write_policies(void) | ||||
| { | ||||
| 	ssize_t written; | ||||
| 	int fd; | ||||
| 
 | ||||
| 	fd = open(add_whitelist_policy_file, O_WRONLY); | ||||
| 	if (fd < 0) | ||||
| 		die("cant open add_whitelist_policy file\n"); | ||||
| 	written = write(fd, "1:2", strlen("1:2")); | ||||
| 	if (written != strlen("1:2")) { | ||||
| 		if (written >= 0) { | ||||
| 			die("short write to %s\n", add_whitelist_policy_file); | ||||
| 		} else { | ||||
| 			die("write to %s failed: %s\n", | ||||
| 				add_whitelist_policy_file, strerror(errno)); | ||||
| 		} | ||||
| 	} | ||||
| 	written = write(fd, "1:3", strlen("1:3")); | ||||
| 	if (written != strlen("1:3")) { | ||||
| 		if (written >= 0) { | ||||
| 			die("short write to %s\n", add_whitelist_policy_file); | ||||
| 		} else { | ||||
| 			die("write to %s failed: %s\n", | ||||
| 				add_whitelist_policy_file, strerror(errno)); | ||||
| 		} | ||||
| 	} | ||||
| 	if (close(fd) != 0) { | ||||
| 		die("close of %s failed: %s\n", | ||||
| 			add_whitelist_policy_file, strerror(errno)); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| static bool test_userns(bool expect_success) | ||||
| { | ||||
| 	uid_t uid; | ||||
| 	char map_file_name[32]; | ||||
| 	size_t sz = sizeof(map_file_name); | ||||
| 	pid_t cpid; | ||||
| 	bool success; | ||||
| 
 | ||||
| 	uid = getuid(); | ||||
| 
 | ||||
| 	int clone_flags = CLONE_NEWUSER; | ||||
| 	cpid = syscall(SYS_clone, clone_flags, NULL); | ||||
| 	if (cpid == -1) { | ||||
| 	    printf("clone failed"); | ||||
| 	    return false; | ||||
| 	} | ||||
| 
 | ||||
| 	if (cpid == 0) {	/* Code executed by child */ | ||||
| 		// Give parent 1 second to write map file
 | ||||
| 		sleep(1); | ||||
| 		exit(EXIT_SUCCESS); | ||||
| 	} else {		/* Code executed by parent */ | ||||
| 		if(snprintf(map_file_name, sz, "/proc/%d/uid_map", cpid) < 0) { | ||||
| 			printf("preparing file name string failed"); | ||||
| 			return false; | ||||
| 		} | ||||
| 		success = write_file(map_file_name, "0 0 1", uid); | ||||
| 		return success == expect_success; | ||||
| 	} | ||||
| 
 | ||||
| 	printf("should not reach here"); | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| static void test_setuid(uid_t child_uid, bool expect_success) | ||||
| { | ||||
| 	pid_t cpid, w; | ||||
| 	int wstatus; | ||||
| 
 | ||||
| 	cpid = fork(); | ||||
| 	if (cpid == -1) { | ||||
| 		die("fork\n"); | ||||
| 	} | ||||
| 
 | ||||
| 	if (cpid == 0) {	    /* Code executed by child */ | ||||
| 		setuid(child_uid); | ||||
| 		if (getuid() == child_uid) | ||||
| 			exit(EXIT_SUCCESS); | ||||
| 		else | ||||
| 			exit(EXIT_FAILURE); | ||||
| 	} else {		 /* Code executed by parent */ | ||||
| 		do { | ||||
| 			w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED); | ||||
| 			if (w == -1) { | ||||
| 				die("waitpid\n"); | ||||
| 			} | ||||
| 
 | ||||
| 			if (WIFEXITED(wstatus)) { | ||||
| 				if (WEXITSTATUS(wstatus) == EXIT_SUCCESS) { | ||||
| 					if (expect_success) { | ||||
| 						return; | ||||
| 					} else { | ||||
| 						die("unexpected success\n"); | ||||
| 					} | ||||
| 				} else { | ||||
| 					if (expect_success) { | ||||
| 						die("unexpected failure\n"); | ||||
| 					} else { | ||||
| 						return; | ||||
| 					} | ||||
| 				} | ||||
| 			} else if (WIFSIGNALED(wstatus)) { | ||||
| 				if (WTERMSIG(wstatus) == 9) { | ||||
| 					if (expect_success) | ||||
| 						die("killed unexpectedly\n"); | ||||
| 					else | ||||
| 						return; | ||||
| 				} else { | ||||
| 					die("unexpected signal: %d\n", wstatus); | ||||
| 				} | ||||
| 			} else { | ||||
| 				die("unexpected status: %d\n", wstatus); | ||||
| 			} | ||||
| 		} while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus)); | ||||
| 	} | ||||
| 
 | ||||
| 	die("should not reach here\n"); | ||||
| } | ||||
| 
 | ||||
| static void ensure_users_exist(void) | ||||
| { | ||||
| 	ensure_user_exists(ROOT_USER); | ||||
| 	ensure_user_exists(RESTRICTED_PARENT); | ||||
| 	ensure_user_exists(ALLOWED_CHILD1); | ||||
| 	ensure_user_exists(ALLOWED_CHILD2); | ||||
| 	ensure_user_exists(NO_POLICY_USER); | ||||
| } | ||||
| 
 | ||||
| static void drop_caps(bool setid_retained) | ||||
| { | ||||
| 	cap_value_t cap_values[] = {CAP_SETUID, CAP_SETGID}; | ||||
| 	cap_t caps; | ||||
| 
 | ||||
| 	caps = cap_get_proc(); | ||||
| 	if (setid_retained) | ||||
| 		cap_set_flag(caps, CAP_EFFECTIVE, 2, cap_values, CAP_SET); | ||||
| 	else | ||||
| 		cap_clear(caps); | ||||
| 	cap_set_proc(caps); | ||||
| 	cap_free(caps); | ||||
| } | ||||
| 
 | ||||
| int main(int argc, char **argv) | ||||
| { | ||||
| 	ensure_users_exist(); | ||||
| 	ensure_securityfs_mounted(); | ||||
| 	write_policies(); | ||||
| 
 | ||||
| 	if (prctl(PR_SET_KEEPCAPS, 1L)) | ||||
| 		die("Error with set keepcaps\n"); | ||||
| 
 | ||||
| 	// First test to make sure we can write userns mappings from a user
 | ||||
| 	// that doesn't have any restrictions (as long as it has CAP_SETUID);
 | ||||
| 	setuid(NO_POLICY_USER); | ||||
| 	setgid(NO_POLICY_USER); | ||||
| 
 | ||||
| 	// Take away all but setid caps
 | ||||
| 	drop_caps(true); | ||||
| 
 | ||||
| 	// Need PR_SET_DUMPABLE flag set so we can write /proc/[pid]/uid_map
 | ||||
| 	// from non-root parent process.
 | ||||
| 	if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) | ||||
| 		die("Error with set dumpable\n"); | ||||
| 
 | ||||
| 	if (!test_userns(true)) { | ||||
| 		die("test_userns failed when it should work\n"); | ||||
| 	} | ||||
| 
 | ||||
| 	setuid(RESTRICTED_PARENT); | ||||
| 	setgid(RESTRICTED_PARENT); | ||||
| 
 | ||||
| 	test_setuid(ROOT_USER, false); | ||||
| 	test_setuid(ALLOWED_CHILD1, true); | ||||
| 	test_setuid(ALLOWED_CHILD2, true); | ||||
| 	test_setuid(NO_POLICY_USER, false); | ||||
| 
 | ||||
| 	if (!test_userns(false)) { | ||||
| 		die("test_userns worked when it should fail\n"); | ||||
| 	} | ||||
| 
 | ||||
| 	// Now take away all caps
 | ||||
| 	drop_caps(false); | ||||
| 	test_setuid(2, false); | ||||
| 	test_setuid(3, false); | ||||
| 	test_setuid(4, false); | ||||
| 
 | ||||
| 	// NOTE: this test doesn't clean up users that were created in
 | ||||
| 	// /etc/passwd or flush policies that were added to the LSM.
 | ||||
| 	return EXIT_SUCCESS; | ||||
| } | ||||
							
								
								
									
										26
									
								
								tools/testing/selftests/safesetid/safesetid-test.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										26
									
								
								tools/testing/selftests/safesetid/safesetid-test.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,26 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| TCID="safesetid-test.sh" | ||||
| errcode=0 | ||||
| 
 | ||||
| # Kselftest framework requirement - SKIP code is 4. | ||||
| ksft_skip=4 | ||||
| 
 | ||||
| check_root() | ||||
| { | ||||
| 	uid=$(id -u) | ||||
| 	if [ $uid -ne 0 ]; then | ||||
| 		echo $TCID: must be run as root >&2 | ||||
| 		exit $ksft_skip | ||||
| 	fi | ||||
| } | ||||
| 
 | ||||
| main_function() | ||||
| { | ||||
|   check_root | ||||
|   ./safesetid-test | ||||
| } | ||||
| 
 | ||||
| main_function | ||||
| echo "$TCID: done" | ||||
| exit $errcode | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user