Commit message: CVE-2022-31214: fixing the fix, one more time

the previous commit "CVE-2022-31214: fixing the fix"
made private-etc=fonts,fonts and similar commands
fail with an error

fix that regression by tolerating already existing
directories
CVE: CVE-2022-31214
CWE: CWE-94
Vulnerability description: A Privilege Context Switching issue was discovered in join.c in Firejail 0.9.68. By crafting a bogus Firejail container that is accepted by the Firejail setuid-root program as a join target, a local attacker can enter an environment in which the Linux user namespace is still the initial user namespace, the NO_NEW_PRIVS prctl is not activated, and the entered mount namespace is under the attacker's control. In this way, the filesystem layout can be adjusted to gain root privileges through execution of available setuid-root binaries such as su or sudo.

Project: firejail

static void duplicate(const char *fname, const char *private_dir, const char *private_run_dir) {
	assert(fname);

	if (*fname == '~' || *fname == '/' || strstr(fname, "..")) {
		fprintf(stderr, "Error: \"%s\" is an invalid filename\n", fname);
		exit(1);
	}
	invalid_filename(fname, 0); // no globbing

	char *src;
	if (asprintf(&src,  "%s/%s", private_dir, fname) == -1)
		errExit("asprintf");
	if (check_dir_or_file(src) == 0) {
		fwarning("skipping %s for private %s\n", fname, private_dir);
		free(src);
		return;
	}

	if (arg_debug)
		printf("Copying %s to private %s\n", src, private_dir);

	char *dst;
	if (asprintf(&dst, "%s/%s", private_run_dir, fname) == -1)
		errExit("asprintf");

	build_dirs(src, dst, strlen(private_dir), strlen(private_run_dir));

	// follow links! this will make a copy of the file or directory pointed by the symlink
	// this will solve problems such as NixOS #4887
	// don't follow links to dynamic directories such as /proc
	if (strcmp(src, "/etc/mtab") == 0)
		sbox_run(SBOX_ROOT | SBOX_SECCOMP, 3, PATH_FCOPY, src, dst);
	else
		sbox_run(SBOX_ROOT | SBOX_SECCOMP, 4, PATH_FCOPY, "--follow-link", src, dst);

	free(dst);
	fs_logger2("clone", src);
	free(src);
}

Project: firejail

void fs_private_dir_list(const char *private_dir, const char *private_run_dir, const char *private_list) {
	timetrace_start();
	fs_private_dir_copy(private_dir, private_run_dir, private_list);
	fs_private_dir_mount(private_dir, private_run_dir);
	fmessage("Private %s installed in %0.2f ms\n", private_dir, timetrace_end());
}

Project: firejail

static void build_dirs(char *src, char *dst, size_t src_prefix_len, size_t dst_prefix_len) {
	char *p = src + src_prefix_len + 1;
	char *q = dst + dst_prefix_len + 1;
	char *r = dst + dst_prefix_len;
	struct stat s;
	bool last = false;
	*r = '\0';
	for (; !last; p++, q++) {
		if (*p == '\0') {
			last = true;
		}
		if (*p == '\0' || (*p == '/' && *(p - 1) != '/')) {
			// We found a new component of our src path.
			// Null-terminate it temporarily here so that we can work
			// with it.
			*p = '\0';
			if (stat(src, &s) == 0 && S_ISDIR(s.st_mode)) {
				// Null-terminate the dst path and undo its previous
				// termination.
				*q = '\0';
				*r = '/';
				r = q;
				if (mkdir(dst, 0700) != 0 && errno != EEXIST)
					errExit("mkdir");
				if (chmod(dst, s.st_mode) != 0)
					errExit("chmod");
			}
			if (!last) {
				// If we're not at the final terminating null, restore
				// the slash so that we can continue our traversal.
				*p = '/';
			}
		}
	}
}

Project: firejail

static int check_dir_or_file(const char *fname) {
	assert(fname);

	struct stat s;
	if (stat(fname, &s) == -1) {
		if (arg_debug)
			fwarning("file %s not found.\n", fname);
		return 0;
	}

	// read access
	if (access(fname, R_OK) == -1)
		goto errexit;

	// dir or regular file
	if (S_ISDIR(s.st_mode) || S_ISREG(s.st_mode) || !is_link(fname))
		return 1;	// normal exit

errexit:
	fprintf(stderr, "Error: invalid file type, %s.\n", fname);
	exit(1);
}

Project: firejail

void fs_private_dir_copy(const char *private_dir, const char *private_run_dir, const char *private_list) {
	assert(private_dir);
	assert(private_run_dir);
	assert(private_list);

	// nothing to do if directory does not exist
	struct stat s;
	if (stat(private_dir, &s) == -1) {
		if (arg_debug)
			printf("Cannot find %s: %s\n", private_dir, strerror(errno));
		return;
	}

	// create /run/firejail/mnt/etc directory
	mkdir_attr(private_run_dir, 0755, 0, 0);
	selinux_relabel_path(private_run_dir, private_dir);
	fs_logger2("tmpfs", private_dir);

	fs_logger_print();	// save the current log


	// copy the list of files in the new etc directory
	// using a new child process with root privileges
	if (*private_list != '\0') {
		if (arg_debug)
			printf("Copying files in the new %s directory:\n", private_dir);

		// copy the list of files in the new home directory
		char *dlist = strdup(private_list);
		if (!dlist)
			errExit("strdup");


		char *ptr = strtok(dlist, ",");
		if (!ptr) {
			fprintf(stderr, "Error: invalid private %s argument\n", private_dir);
			exit(1);
		}
		duplicate(ptr, private_dir, private_run_dir);

		while ((ptr = strtok(NULL, ",")) != NULL)
			duplicate(ptr, private_dir, private_run_dir);
		free(dlist);
		fs_logger_print();
	}
}

Project: firejail

void fs_rebuild_etc(void) {
	int have_dhcp = 1;
	if (cfg.dns1 == NULL && !any_dhcp())
		have_dhcp = 0;

	if (arg_debug)
		printf("rebuilding /etc directory\n");
	if (mkdir(RUN_DNS_ETC, 0755))
		errExit("mkdir");
	selinux_relabel_path(RUN_DNS_ETC, "/etc");
	fs_logger("tmpfs /etc");

	DIR *dir = opendir("/etc");
	if (!dir)
		errExit("opendir");

	struct stat s;
	struct dirent *entry;
	while ((entry = readdir(dir))) {
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
			continue;

		// skip files in cfg.profile_rebuild_etc list
		// these files are already blacklisted
		{
			ProfileEntry *prf = cfg.profile_rebuild_etc;
			int found = 0;
			while (prf) {
				if (strcmp(entry->d_name, prf->data + 5) == 0) { // 5 is strlen("/etc/")
					found = 1;
					break;
				}
				prf = prf->next;
			}
			if (found)
				continue;
		}

		// for resolv.conf we might have to  create a brand new file later
		if (have_dhcp &&
		    (strcmp(entry->d_name, "resolv.conf") == 0 ||
		     strcmp(entry->d_name, "resolv.conf.dhclient-new") == 0))
			continue;
//		printf("linking %s\n", entry->d_name);

		char *src;
		if (asprintf(&src, "/etc/%s", entry->d_name) == -1)
			errExit("asprintf");
		if (stat(src, &s) != 0) {
			free(src);
			continue;
		}

		char *dest;
		if (asprintf(&dest, "%s/%s", RUN_DNS_ETC, entry->d_name) == -1)
			errExit("asprintf");

		int symlink_done = 0;
		if (is_link(src)) {
			char *rp =realpath(src, NULL);
			if (rp == NULL) {
				free(src);
				free(dest);
				continue;
			}
			if (symlink(rp, dest))
				errExit("symlink");
			else
				symlink_done = 1;
		}
		else if (S_ISDIR(s.st_mode))
			create_empty_dir_as_root(dest, S_IRWXU);
		else
			create_empty_file_as_root(dest, S_IRUSR | S_IWUSR);

		// bind-mount src on top of dest
		if (!symlink_done) {
			if (mount(src, dest, NULL, MS_BIND|MS_REC, NULL) < 0)
				errExit("mount bind mirroring /etc");
		}
		fs_logger2("clone", src);

		free(src);
		free(dest);
	}
	closedir(dir);

	// mount bind our private etc directory on top of /etc
	if (arg_debug)
		printf("Mount-bind %s on top of /etc\n", RUN_DNS_ETC);
	if (mount(RUN_DNS_ETC, "/etc", NULL, MS_BIND|MS_REC, NULL) < 0)
		errExit("mount bind mirroring /etc");
	fs_logger("mount /etc");

	if (have_dhcp == 0)
		return;

	if (arg_debug)
		printf("Creating a new /etc/resolv.conf file\n");
	FILE *fp = fopen("/etc/resolv.conf", "wxe");
	if (!fp) {
		fprintf(stderr, "Error: cannot create /etc/resolv.conf file\n");
		exit(1);
	}

	if (cfg.dns1) {
		if (any_dhcp())
			fwarning("network setup uses DHCP, nameservers will likely be overwritten\n");
		fprintf(fp, "nameserver %s\n", cfg.dns1);
	}
	if (cfg.dns2)
		fprintf(fp, "nameserver %s\n", cfg.dns2);
	if (cfg.dns3)
		fprintf(fp, "nameserver %s\n", cfg.dns3);
	if (cfg.dns4)
		fprintf(fp, "nameserver %s\n", cfg.dns4);

	// mode and owner
	SET_PERMS_STREAM(fp, 0, 0, 0644);

	fclose(fp);

	fs_logger("create /etc/resolv.conf");
}

Project: firejail

void fs_private_dir_mount(const char *private_dir, const char *private_run_dir) {
	assert(private_dir);
	assert(private_run_dir);

	if (arg_debug)
		printf("Mount-bind %s on top of %s\n", private_run_dir, private_dir);

	// nothing to do if directory does not exist
	struct stat s;
	if (stat(private_dir, &s) == -1) {
		if (arg_debug)
			printf("Cannot find %s: %s\n", private_dir, strerror(errno));
		return;
	}

	if (mount(private_run_dir, private_dir, NULL, MS_BIND|MS_REC, NULL) < 0)
		errExit("mount bind");
	fs_logger2("mount", private_dir);

	// mask private_run_dir (who knows if there are writable paths, and it is mounted exec)
	if (mount("tmpfs", private_run_dir, "tmpfs", MS_NOSUID | MS_NODEV | MS_STRICTATIME,  "mode=755,gid=0") < 0)
		errExit("mounting tmpfs");
	fs_logger2("tmpfs", private_run_dir);
}

Project: firejail

void fs_machineid(void) {
	union machineid_t {
		uint8_t u8[16];
		uint32_t u32[4];
	} mid;

	// if --machine-id flag is inactive, do nothing
	if (arg_machineid == 0)
		return;
	if (arg_debug)
		printf("Generating a new machine-id\n");

	// init random number generator
	srand(time(NULL));

	// generate random id
	mid.u32[0] = rand();
	mid.u32[1] = rand();
	mid.u32[2] = rand();
	mid.u32[3] = rand();

	// UUID version 4 and DCE variant
	mid.u8[6] = (mid.u8[6] & 0x0F) | 0x40;
	mid.u8[8] = (mid.u8[8] & 0x3F) | 0x80;

	// write it in a file
	FILE *fp = fopen(RUN_MACHINEID, "we");
	if (!fp)
		errExit("fopen");
	fprintf(fp, "%08x%08x%08x%08x\n", mid.u32[0], mid.u32[1], mid.u32[2], mid.u32[3]);
	fclose(fp);
	if (set_perms(RUN_MACHINEID, 0, 0, 0444))
		errExit("set_perms");

	selinux_relabel_path(RUN_MACHINEID, "/etc/machine-id");

	struct stat s;
	if (stat("/etc/machine-id", &s) == 0) {
		if (arg_debug)
			printf("installing a new /etc/machine-id\n");

		if (mount(RUN_MACHINEID, "/etc/machine-id", "none", MS_BIND, "mode=444,gid=0"))
			errExit("mount");
	}
	if (stat("/var/lib/dbus/machine-id", &s) == 0) {
		if (mount(RUN_MACHINEID, "/var/lib/dbus/machine-id", "none", MS_BIND, "mode=444,gid=0"))
			errExit("mount");
	}
}