Blame SOURCES/0008-selinux_restorecon-introduce-selinux_restorecon_para.patch

7d2fd3
From 847282ce385a4fc03092eb10422b1878590e9bdd Mon Sep 17 00:00:00 2001
7d2fd3
From: Ondrej Mosnacek <omosnace@redhat.com>
7d2fd3
Date: Tue, 26 Oct 2021 13:52:38 +0200
7d2fd3
Subject: [PATCH] selinux_restorecon: introduce selinux_restorecon_parallel(3)
7d2fd3
7d2fd3
Refactor selinux_restorecon(3) to allow for distributing the relabeling
7d2fd3
to multiple threads and add a new function
7d2fd3
selinux_restorecon_parallel(3), which allows specifying the number of
7d2fd3
threads to use. The existing selinux_restorecon(3) function maintains
7d2fd3
the same interface and maintains the same behavior (i.e. relabeling is
7d2fd3
done on a single thread).
7d2fd3
7d2fd3
The parallel implementation takes a simple approach of performing all
7d2fd3
the directory tree traversal in a critical section and only letting the
7d2fd3
relabeling of individual objects run in parallel. Thankfully, this
7d2fd3
approach turns out to be efficient enough in practice, as shown by
7d2fd3
restorecon benchmarks (detailed in a subsequent patch that switches
7d2fd3
setfiles & restorecon to use selinux_restorecon_parallel(3)).
7d2fd3
7d2fd3
Note that to be able to use the parallelism, the calling application/
7d2fd3
library must be explicitly linked to the libpthread library (statically
7d2fd3
or dynamically). This is necessary to mantain the requirement that
7d2fd3
libselinux shouldn't explicitly link with libpthread. (I don't know what
7d2fd3
exactly was the reason behind this requirement as the commit logs are
7d2fd3
fuzzy, but special care has been taken in the past to maintain it, so I
7d2fd3
didn't want to break it...)
7d2fd3
7d2fd3
Signed-off-by: Ondrej Mosnacek <omosnace@redhat.com>
7d2fd3
---
7d2fd3
 libselinux/include/selinux/restorecon.h       |  14 +
7d2fd3
 libselinux/man/man3/selinux_restorecon.3      |  29 ++
7d2fd3
 .../man/man3/selinux_restorecon_parallel.3    |   1 +
7d2fd3
 libselinux/src/libselinux.map                 |   5 +
7d2fd3
 libselinux/src/selinux_internal.h             |  16 +
7d2fd3
 libselinux/src/selinux_restorecon.c           | 436 ++++++++++++------
7d2fd3
 libselinux/src/selinuxswig_python.i           |   6 +-
7d2fd3
 libselinux/src/selinuxswig_python_exception.i |   8 +
7d2fd3
 8 files changed, 368 insertions(+), 147 deletions(-)
7d2fd3
 create mode 100644 libselinux/man/man3/selinux_restorecon_parallel.3
7d2fd3
7d2fd3
diff --git a/libselinux/include/selinux/restorecon.h b/libselinux/include/selinux/restorecon.h
7d2fd3
index ca8ce768587a..8f9a030cda98 100644
7d2fd3
--- a/libselinux/include/selinux/restorecon.h
7d2fd3
+++ b/libselinux/include/selinux/restorecon.h
7d2fd3
@@ -2,6 +2,7 @@
7d2fd3
 #define _RESTORECON_H_
7d2fd3
 
7d2fd3
 #include <sys/types.h>
7d2fd3
+#include <stddef.h>
7d2fd3
 #include <stdarg.h>
7d2fd3
 
7d2fd3
 #ifdef __cplusplus
7d2fd3
@@ -23,6 +24,19 @@ extern "C" {
7d2fd3
  */
7d2fd3
 extern int selinux_restorecon(const char *pathname,
7d2fd3
 				    unsigned int restorecon_flags);
7d2fd3
+/**
7d2fd3
+ * selinux_restorecon_parallel - Relabel files, optionally use more threads.
7d2fd3
+ * @pathname: specifies file/directory to relabel.
7d2fd3
+ * @restorecon_flags: specifies the actions to be performed when relabeling.
7d2fd3
+ * @nthreads: specifies the number of threads to use (0 = use number of CPUs
7d2fd3
+ *            currently online)
7d2fd3
+ *
7d2fd3
+ * Same as selinux_restorecon(3), but allows to use multiple threads to do
7d2fd3
+ * the work.
7d2fd3
+ */
7d2fd3
+extern int selinux_restorecon_parallel(const char *pathname,
7d2fd3
+				       unsigned int restorecon_flags,
7d2fd3
+				       size_t nthreads);
7d2fd3
 /*
7d2fd3
  * restorecon_flags options
7d2fd3
  */
7d2fd3
diff --git a/libselinux/man/man3/selinux_restorecon.3 b/libselinux/man/man3/selinux_restorecon.3
7d2fd3
index c4576fe79ff6..500845917fb8 100644
7d2fd3
--- a/libselinux/man/man3/selinux_restorecon.3
7d2fd3
+++ b/libselinux/man/man3/selinux_restorecon.3
7d2fd3
@@ -11,6 +11,14 @@ selinux_restorecon \- restore file(s) default SELinux security contexts
7d2fd3
 .br
7d2fd3
 .BI "unsigned int " restorecon_flags ");"
7d2fd3
 .in
7d2fd3
+.sp
7d2fd3
+.BI "int selinux_restorecon_parallel(const char *" pathname ,
7d2fd3
+.in +\w'int selinux_restorecon_parallel('u
7d2fd3
+.br
7d2fd3
+.BI "unsigned int " restorecon_flags ","
7d2fd3
+.br
7d2fd3
+.BI "size_t " nthreads ");"
7d2fd3
+.in
7d2fd3
 .
7d2fd3
 .SH "DESCRIPTION"
7d2fd3
 .BR selinux_restorecon ()
7d2fd3
@@ -187,6 +195,27 @@ unless the
7d2fd3
 .B SELINUX_RESTORECON_IGNORE_MOUNTS
7d2fd3
 flag has been set.
7d2fd3
 .RE
7d2fd3
+.sp
7d2fd3
+.BR selinux_restorecon_parallel()
7d2fd3
+is similar to
7d2fd3
+.BR selinux_restorecon (3),
7d2fd3
+but accepts another parameter that allows to run relabeling over multiple
7d2fd3
+threads:
7d2fd3
+.sp
7d2fd3
+.RS
7d2fd3
+.IR nthreads
7d2fd3
+specifies the number of threads to use during relabeling. When set to 1,
7d2fd3
+the behavior is the same as calling
7d2fd3
+.BR selinux_restorecon (3).
7d2fd3
+When set to 0, the function will try to use as many threads as there are
7d2fd3
+online CPU cores. When set to any other number, the function will try to use
7d2fd3
+the given number of threads.
7d2fd3
+.sp
7d2fd3
+Note that to use the parallel relabeling capability, the calling process
7d2fd3
+must be linked with the
7d2fd3
+.B libpthread
7d2fd3
+library (either at compile time or dynamically at run time). Otherwise the
7d2fd3
+function will print a warning and fall back to the single threaded mode.
7d2fd3
 .
7d2fd3
 .SH "RETURN VALUE"
7d2fd3
 On success, zero is returned.  On error, \-1 is returned and
7d2fd3
diff --git a/libselinux/man/man3/selinux_restorecon_parallel.3 b/libselinux/man/man3/selinux_restorecon_parallel.3
7d2fd3
new file mode 100644
7d2fd3
index 000000000000..092d8412cc93
7d2fd3
--- /dev/null
7d2fd3
+++ b/libselinux/man/man3/selinux_restorecon_parallel.3
7d2fd3
@@ -0,0 +1 @@
7d2fd3
+.so man3/selinux_restorecon.3
7d2fd3
diff --git a/libselinux/src/libselinux.map b/libselinux/src/libselinux.map
7d2fd3
index 2a368e93f9fd..d138e951ef0d 100644
7d2fd3
--- a/libselinux/src/libselinux.map
7d2fd3
+++ b/libselinux/src/libselinux.map
7d2fd3
@@ -240,3 +240,8 @@ LIBSELINUX_1.0 {
7d2fd3
   local:
7d2fd3
     *;
7d2fd3
 };
7d2fd3
+
7d2fd3
+LIBSELINUX_3.3 {
7d2fd3
+  global:
7d2fd3
+    selinux_restorecon_parallel;
7d2fd3
+} LIBSELINUX_1.0;
7d2fd3
diff --git a/libselinux/src/selinux_internal.h b/libselinux/src/selinux_internal.h
7d2fd3
index 27e9ac532c3f..297dcf26dee3 100644
7d2fd3
--- a/libselinux/src/selinux_internal.h
7d2fd3
+++ b/libselinux/src/selinux_internal.h
7d2fd3
@@ -69,6 +69,22 @@ extern int selinux_page_size ;
7d2fd3
 			pthread_mutex_unlock(LOCK);		\
7d2fd3
 	} while (0)
7d2fd3
 
7d2fd3
+#pragma weak pthread_create
7d2fd3
+#pragma weak pthread_join
7d2fd3
+#pragma weak pthread_cond_init
7d2fd3
+#pragma weak pthread_cond_signal
7d2fd3
+#pragma weak pthread_cond_destroy
7d2fd3
+#pragma weak pthread_cond_wait
7d2fd3
+
7d2fd3
+/* check if all functions needed to do parallel operations are available */
7d2fd3
+#define __pthread_supported (					\
7d2fd3
+	pthread_create &&					\
7d2fd3
+	pthread_join &&						\
7d2fd3
+	pthread_cond_init &&					\
7d2fd3
+	pthread_cond_destroy &&					\
7d2fd3
+	pthread_cond_signal &&					\
7d2fd3
+	pthread_cond_wait					\
7d2fd3
+)
7d2fd3
 
7d2fd3
 #define SELINUXDIR "/etc/selinux/"
7d2fd3
 #define SELINUXCONFIG SELINUXDIR "config"
7d2fd3
diff --git a/libselinux/src/selinux_restorecon.c b/libselinux/src/selinux_restorecon.c
7d2fd3
index 169dfe3ae232..f7e84657d09d 100644
7d2fd3
--- a/libselinux/src/selinux_restorecon.c
7d2fd3
+++ b/libselinux/src/selinux_restorecon.c
7d2fd3
@@ -610,7 +610,7 @@ out:
7d2fd3
 }
7d2fd3
 
7d2fd3
 static int restorecon_sb(const char *pathname, const struct stat *sb,
7d2fd3
-			    struct rest_flags *flags)
7d2fd3
+			    struct rest_flags *flags, bool first)
7d2fd3
 {
7d2fd3
 	char *newcon = NULL;
7d2fd3
 	char *curcon = NULL;
7d2fd3
@@ -639,7 +639,7 @@ static int restorecon_sb(const char *pathname, const struct stat *sb,
7d2fd3
 						    sb->st_mode);
7d2fd3
 
7d2fd3
 	if (rc < 0) {
7d2fd3
-		if (errno == ENOENT && flags->warnonnomatch)
7d2fd3
+		if (errno == ENOENT && flags->warnonnomatch && first)
7d2fd3
 			selinux_log(SELINUX_INFO,
7d2fd3
 				    "Warning no default label for %s\n",
7d2fd3
 				    lookup_path);
7d2fd3
@@ -814,66 +814,215 @@ oom:
7d2fd3
 	goto free;
7d2fd3
 }
7d2fd3
 
7d2fd3
+struct rest_state {
7d2fd3
+	struct rest_flags flags;
7d2fd3
+	dev_t dev_num;
7d2fd3
+	struct statfs sfsb;
7d2fd3
+	bool ignore_digest;
7d2fd3
+	bool setrestorecondigest;
7d2fd3
+	bool parallel;
7d2fd3
 
7d2fd3
-/*
7d2fd3
- * Public API
7d2fd3
- */
7d2fd3
+	FTS *fts;
7d2fd3
+	FTSENT *ftsent_first;
7d2fd3
+	struct dir_hash_node *head, *current;
7d2fd3
+	bool abort;
7d2fd3
+	int error;
7d2fd3
+	int saved_errno;
7d2fd3
+	pthread_mutex_t mutex;
7d2fd3
+};
7d2fd3
 
7d2fd3
-/* selinux_restorecon(3) - Main function that is responsible for labeling */
7d2fd3
-int selinux_restorecon(const char *pathname_orig,
7d2fd3
-		       unsigned int restorecon_flags)
7d2fd3
+static void *selinux_restorecon_thread(void *arg)
7d2fd3
 {
7d2fd3
-	struct rest_flags flags;
7d2fd3
+	struct rest_state *state = arg;
7d2fd3
+	FTS *fts = state->fts;
7d2fd3
+	FTSENT *ftsent;
7d2fd3
+	int error;
7d2fd3
+	char ent_path[PATH_MAX];
7d2fd3
+	struct stat ent_st;
7d2fd3
+	bool first = false;
7d2fd3
+
7d2fd3
+	if (state->parallel)
7d2fd3
+		pthread_mutex_lock(&state->mutex);
7d2fd3
+
7d2fd3
+	if (state->ftsent_first) {
7d2fd3
+		ftsent = state->ftsent_first;
7d2fd3
+		state->ftsent_first = NULL;
7d2fd3
+		first = true;
7d2fd3
+		goto loop_body;
7d2fd3
+	}
7d2fd3
+
7d2fd3
+	while (((void)(errno = 0), ftsent = fts_read(fts)) != NULL) {
7d2fd3
+loop_body:
7d2fd3
+		/* If the FTS_XDEV flag is set and the device is different */
7d2fd3
+		if (state->flags.set_xdev &&
7d2fd3
+		    ftsent->fts_statp->st_dev != state->dev_num)
7d2fd3
+			continue;
7d2fd3
+
7d2fd3
+		switch (ftsent->fts_info) {
7d2fd3
+		case FTS_DC:
7d2fd3
+			selinux_log(SELINUX_ERROR,
7d2fd3
+				    "Directory cycle on %s.\n",
7d2fd3
+				    ftsent->fts_path);
7d2fd3
+			errno = ELOOP;
7d2fd3
+			state->error = -1;
7d2fd3
+			state->abort = true;
7d2fd3
+			goto finish;
7d2fd3
+		case FTS_DP:
7d2fd3
+			continue;
7d2fd3
+		case FTS_DNR:
7d2fd3
+			error = errno;
7d2fd3
+			errno = ftsent->fts_errno;
7d2fd3
+			selinux_log(SELINUX_ERROR,
7d2fd3
+				    "Could not read %s: %m.\n",
7d2fd3
+				    ftsent->fts_path);
7d2fd3
+			errno = error;
7d2fd3
+			fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
+			continue;
7d2fd3
+		case FTS_NS:
7d2fd3
+			error = errno;
7d2fd3
+			errno = ftsent->fts_errno;
7d2fd3
+			selinux_log(SELINUX_ERROR,
7d2fd3
+				    "Could not stat %s: %m.\n",
7d2fd3
+				    ftsent->fts_path);
7d2fd3
+			errno = error;
7d2fd3
+			fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
+			continue;
7d2fd3
+		case FTS_ERR:
7d2fd3
+			error = errno;
7d2fd3
+			errno = ftsent->fts_errno;
7d2fd3
+			selinux_log(SELINUX_ERROR,
7d2fd3
+				    "Error on %s: %m.\n",
7d2fd3
+				    ftsent->fts_path);
7d2fd3
+			errno = error;
7d2fd3
+			fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
+			continue;
7d2fd3
+		case FTS_D:
7d2fd3
+			if (state->sfsb.f_type == SYSFS_MAGIC &&
7d2fd3
+			    !selabel_partial_match(fc_sehandle,
7d2fd3
+			    ftsent->fts_path)) {
7d2fd3
+				fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
+				continue;
7d2fd3
+			}
7d2fd3
+
7d2fd3
+			if (check_excluded(ftsent->fts_path)) {
7d2fd3
+				fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
+				continue;
7d2fd3
+			}
7d2fd3
 
7d2fd3
-	flags.nochange = (restorecon_flags &
7d2fd3
+			if (state->setrestorecondigest) {
7d2fd3
+				struct dir_hash_node *new_node = NULL;
7d2fd3
+
7d2fd3
+				if (check_context_match_for_dir(ftsent->fts_path,
7d2fd3
+								&new_node,
7d2fd3
+								state->error) &&
7d2fd3
+								!state->ignore_digest) {
7d2fd3
+					selinux_log(SELINUX_INFO,
7d2fd3
+						"Skipping restorecon on directory(%s)\n",
7d2fd3
+						    ftsent->fts_path);
7d2fd3
+					fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
+					continue;
7d2fd3
+				}
7d2fd3
+
7d2fd3
+				if (new_node && !state->error) {
7d2fd3
+					if (!state->current) {
7d2fd3
+						state->current = new_node;
7d2fd3
+						state->head = state->current;
7d2fd3
+					} else {
7d2fd3
+						state->current->next = new_node;
7d2fd3
+						state->current = new_node;
7d2fd3
+					}
7d2fd3
+				}
7d2fd3
+			}
7d2fd3
+			/* fall through */
7d2fd3
+		default:
7d2fd3
+			strcpy(ent_path, ftsent->fts_path);
7d2fd3
+			ent_st = *ftsent->fts_statp;
7d2fd3
+			if (state->parallel)
7d2fd3
+				pthread_mutex_unlock(&state->mutex);
7d2fd3
+
7d2fd3
+			error = restorecon_sb(ent_path, &ent_st, &state->flags,
7d2fd3
+					      first);
7d2fd3
+
7d2fd3
+			if (state->parallel) {
7d2fd3
+				pthread_mutex_lock(&state->mutex);
7d2fd3
+				if (state->abort)
7d2fd3
+					goto unlock;
7d2fd3
+			}
7d2fd3
+
7d2fd3
+			state->error |= error;
7d2fd3
+			first = false;
7d2fd3
+			if (error && state->flags.abort_on_error) {
7d2fd3
+				state->abort = true;
7d2fd3
+				goto finish;
7d2fd3
+			}
7d2fd3
+			break;
7d2fd3
+		}
7d2fd3
+	}
7d2fd3
+
7d2fd3
+finish:
7d2fd3
+	if (!state->saved_errno)
7d2fd3
+		state->saved_errno = errno;
7d2fd3
+unlock:
7d2fd3
+	if (state->parallel)
7d2fd3
+		pthread_mutex_unlock(&state->mutex);
7d2fd3
+	return NULL;
7d2fd3
+}
7d2fd3
+
7d2fd3
+static int selinux_restorecon_common(const char *pathname_orig,
7d2fd3
+				     unsigned int restorecon_flags,
7d2fd3
+				     size_t nthreads)
7d2fd3
+{
7d2fd3
+	struct rest_state state;
7d2fd3
+
7d2fd3
+	state.flags.nochange = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_NOCHANGE) ? true : false;
7d2fd3
-	flags.verbose = (restorecon_flags &
7d2fd3
+	state.flags.verbose = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_VERBOSE) ? true : false;
7d2fd3
-	flags.progress = (restorecon_flags &
7d2fd3
+	state.flags.progress = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_PROGRESS) ? true : false;
7d2fd3
-	flags.mass_relabel = (restorecon_flags &
7d2fd3
+	state.flags.mass_relabel = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_MASS_RELABEL) ? true : false;
7d2fd3
-	flags.recurse = (restorecon_flags &
7d2fd3
+	state.flags.recurse = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_RECURSE) ? true : false;
7d2fd3
-	flags.set_specctx = (restorecon_flags &
7d2fd3
+	state.flags.set_specctx = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_SET_SPECFILE_CTX) ? true : false;
7d2fd3
-	flags.userealpath = (restorecon_flags &
7d2fd3
+	state.flags.userealpath = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_REALPATH) ? true : false;
7d2fd3
-	flags.set_xdev = (restorecon_flags &
7d2fd3
+	state.flags.set_xdev = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_XDEV) ? true : false;
7d2fd3
-	flags.add_assoc = (restorecon_flags &
7d2fd3
+	state.flags.add_assoc = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_ADD_ASSOC) ? true : false;
7d2fd3
-	flags.abort_on_error = (restorecon_flags &
7d2fd3
+	state.flags.abort_on_error = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_ABORT_ON_ERROR) ? true : false;
7d2fd3
-	flags.syslog_changes = (restorecon_flags &
7d2fd3
+	state.flags.syslog_changes = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_SYSLOG_CHANGES) ? true : false;
7d2fd3
-	flags.log_matches = (restorecon_flags &
7d2fd3
+	state.flags.log_matches = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_LOG_MATCHES) ? true : false;
7d2fd3
-	flags.ignore_noent = (restorecon_flags &
7d2fd3
+	state.flags.ignore_noent = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_IGNORE_NOENTRY) ? true : false;
7d2fd3
-	flags.warnonnomatch = true;
7d2fd3
-	flags.conflicterror = (restorecon_flags &
7d2fd3
+	state.flags.warnonnomatch = true;
7d2fd3
+	state.flags.conflicterror = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_CONFLICT_ERROR) ? true : false;
7d2fd3
 	ignore_mounts = (restorecon_flags &
7d2fd3
 		   SELINUX_RESTORECON_IGNORE_MOUNTS) ? true : false;
7d2fd3
-	bool ignore_digest = (restorecon_flags &
7d2fd3
+	state.ignore_digest = (restorecon_flags &
7d2fd3
 		    SELINUX_RESTORECON_IGNORE_DIGEST) ? true : false;
7d2fd3
-	bool setrestorecondigest = true;
7d2fd3
+	state.setrestorecondigest = true;
7d2fd3
+
7d2fd3
+	state.head = NULL;
7d2fd3
+	state.current = NULL;
7d2fd3
+	state.abort = false;
7d2fd3
+	state.error = 0;
7d2fd3
+	state.saved_errno = 0;
7d2fd3
 
7d2fd3
 	struct stat sb;
7d2fd3
-	struct statfs sfsb;
7d2fd3
-	FTS *fts;
7d2fd3
-	FTSENT *ftsent;
7d2fd3
 	char *pathname = NULL, *pathdnamer = NULL, *pathdname, *pathbname;
7d2fd3
 	char *paths[2] = { NULL, NULL };
7d2fd3
 	int fts_flags, error, sverrno;
7d2fd3
-	dev_t dev_num = 0;
7d2fd3
 	struct dir_hash_node *current = NULL;
7d2fd3
-	struct dir_hash_node *head = NULL;
7d2fd3
-	int errno_tmp;
7d2fd3
 
7d2fd3
-	if (flags.verbose && flags.progress)
7d2fd3
-		flags.verbose = false;
7d2fd3
+	if (state.flags.verbose && state.flags.progress)
7d2fd3
+		state.flags.verbose = false;
7d2fd3
 
7d2fd3
 	__selinux_once(fc_once, restorecon_init);
7d2fd3
 
7d2fd3
@@ -886,13 +1035,31 @@ int selinux_restorecon(const char *pathname_orig,
7d2fd3
 	 */
7d2fd3
 	if (selabel_no_digest ||
7d2fd3
 	    (restorecon_flags & SELINUX_RESTORECON_SKIP_DIGEST))
7d2fd3
-		setrestorecondigest = false;
7d2fd3
+		state.setrestorecondigest = false;
7d2fd3
+
7d2fd3
+	if (!__pthread_supported) {
7d2fd3
+		if (nthreads != 1) {
7d2fd3
+			nthreads = 1;
7d2fd3
+			selinux_log(SELINUX_WARNING,
7d2fd3
+				"Threading functionality not available, falling back to 1 thread.");
7d2fd3
+		}
7d2fd3
+	} else if (nthreads == 0) {
7d2fd3
+		long nproc = sysconf(_SC_NPROCESSORS_ONLN);
7d2fd3
+
7d2fd3
+		if (nproc > 0) {
7d2fd3
+			nthreads = nproc;
7d2fd3
+		} else {
7d2fd3
+			nthreads = 1;
7d2fd3
+			selinux_log(SELINUX_WARNING,
7d2fd3
+				"Unable to detect CPU count, falling back to 1 thread.");
7d2fd3
+		}
7d2fd3
+	}
7d2fd3
 
7d2fd3
 	/*
7d2fd3
 	 * Convert passed-in pathname to canonical pathname by resolving
7d2fd3
 	 * realpath of containing dir, then appending last component name.
7d2fd3
 	 */
7d2fd3
-	if (flags.userealpath) {
7d2fd3
+	if (state.flags.userealpath) {
7d2fd3
 		char *basename_cpy = strdup(pathname_orig);
7d2fd3
 		if (!basename_cpy)
7d2fd3
 			goto realpatherr;
7d2fd3
@@ -937,7 +1104,7 @@ int selinux_restorecon(const char *pathname_orig,
7d2fd3
 	paths[0] = pathname;
7d2fd3
 
7d2fd3
 	if (lstat(pathname, &sb) < 0) {
7d2fd3
-		if (flags.ignore_noent && errno == ENOENT) {
7d2fd3
+		if (state.flags.ignore_noent && errno == ENOENT) {
7d2fd3
 			free(pathdnamer);
7d2fd3
 			free(pathname);
7d2fd3
 			return 0;
7d2fd3
@@ -952,21 +1119,21 @@ int selinux_restorecon(const char *pathname_orig,
7d2fd3
 
7d2fd3
 	/* Skip digest if not a directory */
7d2fd3
 	if (!S_ISDIR(sb.st_mode))
7d2fd3
-		setrestorecondigest = false;
7d2fd3
+		state.setrestorecondigest = false;
7d2fd3
 
7d2fd3
-	if (!flags.recurse) {
7d2fd3
+	if (!state.flags.recurse) {
7d2fd3
 		if (check_excluded(pathname)) {
7d2fd3
 			error = 0;
7d2fd3
 			goto cleanup;
7d2fd3
 		}
7d2fd3
 
7d2fd3
-		error = restorecon_sb(pathname, &sb, &flags);
7d2fd3
+		error = restorecon_sb(pathname, &sb, &state.flags, true);
7d2fd3
 		goto cleanup;
7d2fd3
 	}
7d2fd3
 
7d2fd3
 	/* Obtain fs type */
7d2fd3
-	memset(&sfsb, 0, sizeof sfsb);
7d2fd3
-	if (!S_ISLNK(sb.st_mode) && statfs(pathname, &sfsb) < 0) {
7d2fd3
+	memset(&state.sfsb, 0, sizeof(state.sfsb));
7d2fd3
+	if (!S_ISLNK(sb.st_mode) && statfs(pathname, &state.sfsb) < 0) {
7d2fd3
 		selinux_log(SELINUX_ERROR,
7d2fd3
 			    "statfs(%s) failed: %m\n",
7d2fd3
 			    pathname);
7d2fd3
@@ -975,21 +1142,21 @@ int selinux_restorecon(const char *pathname_orig,
7d2fd3
 	}
7d2fd3
 
7d2fd3
 	/* Skip digest on in-memory filesystems and /sys */
7d2fd3
-	if (sfsb.f_type == RAMFS_MAGIC || sfsb.f_type == TMPFS_MAGIC ||
7d2fd3
-	    sfsb.f_type == SYSFS_MAGIC)
7d2fd3
-		setrestorecondigest = false;
7d2fd3
+	if (state.sfsb.f_type == RAMFS_MAGIC || state.sfsb.f_type == TMPFS_MAGIC ||
7d2fd3
+	    state.sfsb.f_type == SYSFS_MAGIC)
7d2fd3
+		state.setrestorecondigest = false;
7d2fd3
 
7d2fd3
-	if (flags.set_xdev)
7d2fd3
+	if (state.flags.set_xdev)
7d2fd3
 		fts_flags = FTS_PHYSICAL | FTS_NOCHDIR | FTS_XDEV;
7d2fd3
 	else
7d2fd3
 		fts_flags = FTS_PHYSICAL | FTS_NOCHDIR;
7d2fd3
 
7d2fd3
-	fts = fts_open(paths, fts_flags, NULL);
7d2fd3
-	if (!fts)
7d2fd3
+	state.fts = fts_open(paths, fts_flags, NULL);
7d2fd3
+	if (!state.fts)
7d2fd3
 		goto fts_err;
7d2fd3
 
7d2fd3
-	ftsent = fts_read(fts);
7d2fd3
-	if (!ftsent)
7d2fd3
+	state.ftsent_first = fts_read(state.fts);
7d2fd3
+	if (!state.ftsent_first)
7d2fd3
 		goto fts_err;
7d2fd3
 
7d2fd3
 	/*
7d2fd3
@@ -1001,106 +1168,66 @@ int selinux_restorecon(const char *pathname_orig,
7d2fd3
 	 * directories with a different device number when the FTS_XDEV flag
7d2fd3
 	 * is set (from http://marc.info/?l=selinux&m=124688830500777&w=2).
7d2fd3
 	 */
7d2fd3
-	dev_num = ftsent->fts_statp->st_dev;
7d2fd3
+	state.dev_num = state.ftsent_first->fts_statp->st_dev;
7d2fd3
 
7d2fd3
-	error = 0;
7d2fd3
-	do {
7d2fd3
-		/* If the FTS_XDEV flag is set and the device is different */
7d2fd3
-		if (flags.set_xdev && ftsent->fts_statp->st_dev != dev_num)
7d2fd3
-			continue;
7d2fd3
+	if (nthreads == 1) {
7d2fd3
+		state.parallel = false;
7d2fd3
+		selinux_restorecon_thread(&state);
7d2fd3
+	} else {
7d2fd3
+		size_t i;
7d2fd3
+		pthread_t self = pthread_self();
7d2fd3
+		pthread_t *threads = NULL;
7d2fd3
 
7d2fd3
-		switch (ftsent->fts_info) {
7d2fd3
-		case FTS_DC:
7d2fd3
-			selinux_log(SELINUX_ERROR,
7d2fd3
-				    "Directory cycle on %s.\n",
7d2fd3
-				    ftsent->fts_path);
7d2fd3
-			errno = ELOOP;
7d2fd3
-			error = -1;
7d2fd3
-			goto out;
7d2fd3
-		case FTS_DP:
7d2fd3
-			continue;
7d2fd3
-		case FTS_DNR:
7d2fd3
-			errno_tmp = errno;
7d2fd3
-			errno = ftsent->fts_errno;
7d2fd3
-			selinux_log(SELINUX_ERROR,
7d2fd3
-				    "Could not read %s: %m.\n",
7d2fd3
-				    ftsent->fts_path);
7d2fd3
-			errno = errno_tmp;
7d2fd3
-			fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
-			continue;
7d2fd3
-		case FTS_NS:
7d2fd3
-			errno_tmp = errno;
7d2fd3
-			errno = ftsent->fts_errno;
7d2fd3
-			selinux_log(SELINUX_ERROR,
7d2fd3
-				    "Could not stat %s: %m.\n",
7d2fd3
-				    ftsent->fts_path);
7d2fd3
-			errno = errno_tmp;
7d2fd3
-			fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
-			continue;
7d2fd3
-		case FTS_ERR:
7d2fd3
-			errno_tmp = errno;
7d2fd3
-			errno = ftsent->fts_errno;
7d2fd3
-			selinux_log(SELINUX_ERROR,
7d2fd3
-				    "Error on %s: %m.\n",
7d2fd3
-				    ftsent->fts_path);
7d2fd3
-			errno = errno_tmp;
7d2fd3
-			fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
-			continue;
7d2fd3
-		case FTS_D:
7d2fd3
-			if (sfsb.f_type == SYSFS_MAGIC &&
7d2fd3
-			    !selabel_partial_match(fc_sehandle,
7d2fd3
-			    ftsent->fts_path)) {
7d2fd3
-				fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
-				continue;
7d2fd3
-			}
7d2fd3
+		pthread_mutex_init(&state.mutex, NULL);
7d2fd3
 
7d2fd3
-			if (check_excluded(ftsent->fts_path)) {
7d2fd3
-				fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
-				continue;
7d2fd3
+		threads = calloc(nthreads - 1, sizeof(*threads));
7d2fd3
+		if (!threads)
7d2fd3
+			goto oom;
7d2fd3
+
7d2fd3
+		state.parallel = true;
7d2fd3
+		/*
7d2fd3
+		 * Start (nthreads - 1) threads - the main thread is going to
7d2fd3
+		 * take part, too.
7d2fd3
+		 */
7d2fd3
+		for (i = 0; i < nthreads - 1; i++) {
7d2fd3
+			if (pthread_create(&threads[i], NULL,
7d2fd3
+					   selinux_restorecon_thread, &state)) {
7d2fd3
+				/*
7d2fd3
+				 * If any thread fails to be created, just mark
7d2fd3
+				 * it as such and let the successfully created
7d2fd3
+				 * threads do the job. In the worst case the
7d2fd3
+				 * main thread will do everything, but that's
7d2fd3
+				 * still better than to give up.
7d2fd3
+				 */
7d2fd3
+				threads[i] = self;
7d2fd3
 			}
7d2fd3
+		}
7d2fd3
 
7d2fd3
-			if (setrestorecondigest) {
7d2fd3
-				struct dir_hash_node *new_node = NULL;
7d2fd3
+		/* Let's join in on the fun! */
7d2fd3
+		selinux_restorecon_thread(&state);
7d2fd3
 
7d2fd3
-				if (check_context_match_for_dir(ftsent->fts_path,
7d2fd3
-								&new_node,
7d2fd3
-								error) &&
7d2fd3
-								!ignore_digest) {
7d2fd3
-					selinux_log(SELINUX_INFO,
7d2fd3
-						    "Skipping restorecon on directory(%s)\n",
7d2fd3
-						    ftsent->fts_path);
7d2fd3
-					fts_set(fts, ftsent, FTS_SKIP);
7d2fd3
-					continue;
7d2fd3
-				}
7d2fd3
-
7d2fd3
-				if (new_node && !error) {
7d2fd3
-					if (!current) {
7d2fd3
-						current = new_node;
7d2fd3
-						head = current;
7d2fd3
-					} else {
7d2fd3
-						current->next = new_node;
7d2fd3
-						current = current->next;
7d2fd3
-					}
7d2fd3
-				}
7d2fd3
-			}
7d2fd3
-			/* fall through */
7d2fd3
-		default:
7d2fd3
-			error |= restorecon_sb(ftsent->fts_path,
7d2fd3
-					       ftsent->fts_statp, &flags);
7d2fd3
-			if (flags.warnonnomatch)
7d2fd3
-				flags.warnonnomatch = false;
7d2fd3
-			if (error && flags.abort_on_error)
7d2fd3
-				goto out;
7d2fd3
-			break;
7d2fd3
+		/* Now wait for all threads to finish. */
7d2fd3
+		for (i = 0; i < nthreads - 1; i++) {
7d2fd3
+			/* Skip threads that failed to be created. */
7d2fd3
+			if (pthread_equal(threads[i], self))
7d2fd3
+				continue;
7d2fd3
+			pthread_join(threads[i], NULL);
7d2fd3
 		}
7d2fd3
-	} while ((ftsent = fts_read(fts)) != NULL);
7d2fd3
+		free(threads);
7d2fd3
+
7d2fd3
+		pthread_mutex_destroy(&state.mutex);
7d2fd3
+	}
7d2fd3
+
7d2fd3
+	error = state.error;
7d2fd3
+	if (state.saved_errno)
7d2fd3
+		goto out;
7d2fd3
 
7d2fd3
 	/*
7d2fd3
 	 * Labeling successful. Write partial match digests for subdirectories.
7d2fd3
 	 * TODO: Write digest upon FTS_DP if no error occurs in its descents.
7d2fd3
 	 */
7d2fd3
-	if (setrestorecondigest && !flags.nochange && !error) {
7d2fd3
-		current = head;
7d2fd3
+	if (state.setrestorecondigest && !state.flags.nochange && !error) {
7d2fd3
+		current = state.head;
7d2fd3
 		while (current != NULL) {
7d2fd3
 			if (setxattr(current->path,
7d2fd3
 			    RESTORECON_PARTIAL_MATCH_DIGEST,
7d2fd3
@@ -1115,22 +1242,21 @@ int selinux_restorecon(const char *pathname_orig,
7d2fd3
 	}
7d2fd3
 
7d2fd3
 out:
7d2fd3
-	if (flags.progress && flags.mass_relabel)
7d2fd3
+	if (state.flags.progress && state.flags.mass_relabel)
7d2fd3
 		fprintf(stdout, "\r%s 100.0%%\n", pathname);
7d2fd3
 
7d2fd3
-	sverrno = errno;
7d2fd3
-	(void) fts_close(fts);
7d2fd3
-	errno = sverrno;
7d2fd3
+	(void) fts_close(state.fts);
7d2fd3
+	errno = state.saved_errno;
7d2fd3
 cleanup:
7d2fd3
-	if (flags.add_assoc) {
7d2fd3
-		if (flags.verbose)
7d2fd3
+	if (state.flags.add_assoc) {
7d2fd3
+		if (state.flags.verbose)
7d2fd3
 			filespec_eval();
7d2fd3
 		filespec_destroy();
7d2fd3
 	}
7d2fd3
 	free(pathdnamer);
7d2fd3
 	free(pathname);
7d2fd3
 
7d2fd3
-	current = head;
7d2fd3
+	current = state.head;
7d2fd3
 	while (current != NULL) {
7d2fd3
 		struct dir_hash_node *next = current->next;
7d2fd3
 
7d2fd3
@@ -1164,6 +1290,26 @@ fts_err:
7d2fd3
 	goto cleanup;
7d2fd3
 }
7d2fd3
 
7d2fd3
+
7d2fd3
+/*
7d2fd3
+ * Public API
7d2fd3
+ */
7d2fd3
+
7d2fd3
+/* selinux_restorecon(3) - Main function that is responsible for labeling */
7d2fd3
+int selinux_restorecon(const char *pathname_orig,
7d2fd3
+		       unsigned int restorecon_flags)
7d2fd3
+{
7d2fd3
+	return selinux_restorecon_common(pathname_orig, restorecon_flags, 1);
7d2fd3
+}
7d2fd3
+
7d2fd3
+/* selinux_restorecon_parallel(3) - Parallel version of selinux_restorecon(3) */
7d2fd3
+int selinux_restorecon_parallel(const char *pathname_orig,
7d2fd3
+				unsigned int restorecon_flags,
7d2fd3
+				size_t nthreads)
7d2fd3
+{
7d2fd3
+	return selinux_restorecon_common(pathname_orig, restorecon_flags, nthreads);
7d2fd3
+}
7d2fd3
+
7d2fd3
 /* selinux_restorecon_set_sehandle(3) is called to set the global fc handle */
7d2fd3
 void selinux_restorecon_set_sehandle(struct selabel_handle *hndl)
7d2fd3
 {
7d2fd3
diff --git a/libselinux/src/selinuxswig_python.i b/libselinux/src/selinuxswig_python.i
7d2fd3
index 4c73bf92df96..17e03b9e36a5 100644
7d2fd3
--- a/libselinux/src/selinuxswig_python.i
7d2fd3
+++ b/libselinux/src/selinuxswig_python.i
7d2fd3
@@ -20,7 +20,7 @@ DISABLED = -1
7d2fd3
 PERMISSIVE = 0
7d2fd3
 ENFORCING = 1
7d2fd3
 
7d2fd3
-def restorecon(path, recursive=False, verbose=False, force=False):
7d2fd3
+def restorecon(path, recursive=False, verbose=False, force=False, nthreads=1):
7d2fd3
     """ Restore SELinux context on a given path
7d2fd3
 
7d2fd3
     Arguments:
7d2fd3
@@ -32,6 +32,8 @@ def restorecon(path, recursive=False, verbose=False, force=False):
7d2fd3
     force -- Force reset of context to match file_context for customizable files,
7d2fd3
     and the default file context, changing the user, role, range portion  as well
7d2fd3
     as the type (default False)
7d2fd3
+    nthreads -- The number of threads to use during relabeling, or 0 to use as many
7d2fd3
+    threads as there are online CPU cores (default 1)
7d2fd3
     """
7d2fd3
 
7d2fd3
     restorecon_flags = SELINUX_RESTORECON_IGNORE_DIGEST | SELINUX_RESTORECON_REALPATH
7d2fd3
@@ -41,7 +43,7 @@ def restorecon(path, recursive=False, verbose=False, force=False):
7d2fd3
         restorecon_flags |= SELINUX_RESTORECON_VERBOSE
7d2fd3
     if force:
7d2fd3
         restorecon_flags |= SELINUX_RESTORECON_SET_SPECFILE_CTX
7d2fd3
-    selinux_restorecon(os.path.expanduser(path), restorecon_flags)
7d2fd3
+    selinux_restorecon_parallel(os.path.expanduser(path), restorecon_flags, nthreads)
7d2fd3
 
7d2fd3
 def chcon(path, context, recursive=False):
7d2fd3
     """ Set the SELinux context on a given path """
7d2fd3
diff --git a/libselinux/src/selinuxswig_python_exception.i b/libselinux/src/selinuxswig_python_exception.i
7d2fd3
index 237ea69ad5f5..a02f4923a1e7 100644
7d2fd3
--- a/libselinux/src/selinuxswig_python_exception.i
7d2fd3
+++ b/libselinux/src/selinuxswig_python_exception.i
7d2fd3
@@ -1183,6 +1183,14 @@
7d2fd3
   }
7d2fd3
 }
7d2fd3
 
7d2fd3
+%exception selinux_restorecon_parallel {
7d2fd3
+  $action
7d2fd3
+  if (result < 0) {
7d2fd3
+     PyErr_SetFromErrno(PyExc_OSError);
7d2fd3
+     SWIG_fail;
7d2fd3
+  }
7d2fd3
+}
7d2fd3
+
7d2fd3
 %exception selinux_restorecon_set_alt_rootpath {
7d2fd3
   $action
7d2fd3
   if (result < 0) {
7d2fd3
-- 
7d2fd3
2.33.1
7d2fd3