| /* |
| * The majority of this code is from Android's |
| * external/libselinux/src/android.c and upstream |
| * selinux/policycoreutils/setfiles/restore.c |
| * |
| * See selinux_restorecon(3) for details. |
| */ |
| |
| #include <unistd.h> |
| #include <string.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <stdbool.h> |
| #include <ctype.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <fts.h> |
| #include <inttypes.h> |
| #include <limits.h> |
| #include <stdint.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/xattr.h> |
| #include <sys/vfs.h> |
| #include <sys/statvfs.h> |
| #include <sys/utsname.h> |
| #include <linux/magic.h> |
| #include <libgen.h> |
| #include <syslog.h> |
| #include <assert.h> |
| |
| #include <selinux/selinux.h> |
| #include <selinux/context.h> |
| #include <selinux/label.h> |
| #include <selinux/restorecon.h> |
| |
| #include "callbacks.h" |
| #include "selinux_internal.h" |
| #include "label_file.h" |
| #include "sha1.h" |
| |
| #define STAR_COUNT 1024 |
| |
| static struct selabel_handle *fc_sehandle = NULL; |
| static bool selabel_no_digest; |
| static char *rootpath = NULL; |
| static size_t rootpathlen; |
| |
| /* Information on excluded fs and directories. */ |
| struct edir { |
| char *directory; |
| size_t size; |
| /* True if excluded by selinux_restorecon_set_exclude_list(3). */ |
| bool caller_excluded; |
| }; |
| #define CALLER_EXCLUDED true |
| static bool ignore_mounts; |
| static uint64_t exclude_non_seclabel_mounts(void); |
| static int exclude_count = 0; |
| static struct edir *exclude_lst = NULL; |
| static uint64_t fc_count = 0; /* Number of files processed so far */ |
| static uint64_t efile_count; /* Estimated total number of files */ |
| static pthread_mutex_t progress_mutex = PTHREAD_MUTEX_INITIALIZER; |
| |
| /* Store information on directories with xattr's. */ |
| static struct dir_xattr *dir_xattr_list; |
| static struct dir_xattr *dir_xattr_last; |
| |
| /* Number of errors ignored during the file tree walk. */ |
| static long unsigned skipped_errors; |
| |
| /* restorecon_flags for passing to restorecon_sb() */ |
| struct rest_flags { |
| bool nochange; |
| bool verbose; |
| bool progress; |
| bool mass_relabel; |
| bool set_specctx; |
| bool add_assoc; |
| bool recurse; |
| bool userealpath; |
| bool set_xdev; |
| bool abort_on_error; |
| bool syslog_changes; |
| bool log_matches; |
| bool ignore_noent; |
| bool warnonnomatch; |
| bool conflicterror; |
| bool count_errors; |
| }; |
| |
| static void restorecon_init(void) |
| { |
| struct selabel_handle *sehandle = NULL; |
| |
| if (!fc_sehandle) { |
| sehandle = selinux_restorecon_default_handle(); |
| selinux_restorecon_set_sehandle(sehandle); |
| } |
| |
| efile_count = 0; |
| if (!ignore_mounts) |
| efile_count = exclude_non_seclabel_mounts(); |
| } |
| |
| static pthread_once_t fc_once = PTHREAD_ONCE_INIT; |
| |
| /* |
| * Manage excluded directories: |
| * remove_exclude() - This removes any conflicting entries as there could be |
| * a case where a non-seclabel fs is mounted on /foo and |
| * then a seclabel fs is mounted on top of it. |
| * However if an entry has been added via |
| * selinux_restorecon_set_exclude_list(3) do not remove. |
| * |
| * add_exclude() - Add a directory/fs to be excluded from labeling. If it |
| * has already been added, then ignore. |
| * |
| * check_excluded() - Check if directory/fs is to be excluded when relabeling. |
| * |
| * file_system_count() - Calculates the number of files to be processed. |
| * The count is only used if SELINUX_RESTORECON_PROGRESS |
| * is set and a mass relabel is requested. |
| * |
| * exclude_non_seclabel_mounts() - Reads /proc/mounts to determine what |
| * non-seclabel mounts to exclude from |
| * relabeling. restorecon_init() will not |
| * call this function if the |
| * SELINUX_RESTORECON_IGNORE_MOUNTS |
| * flag is set. |
| * Setting SELINUX_RESTORECON_IGNORE_MOUNTS |
| * is useful where there is a non-seclabel fs |
| * mounted on /foo and then a seclabel fs is |
| * mounted on a directory below this. |
| */ |
| static void remove_exclude(const char *directory) |
| { |
| int i; |
| |
| for (i = 0; i < exclude_count; i++) { |
| if (strcmp(directory, exclude_lst[i].directory) == 0 && |
| !exclude_lst[i].caller_excluded) { |
| free(exclude_lst[i].directory); |
| if (i != exclude_count - 1) |
| exclude_lst[i] = exclude_lst[exclude_count - 1]; |
| exclude_count--; |
| return; |
| } |
| } |
| } |
| |
| static int add_exclude(const char *directory, bool who) |
| { |
| struct edir *tmp_list, *current; |
| size_t len = 0; |
| int i; |
| |
| /* Check if already present. */ |
| for (i = 0; i < exclude_count; i++) { |
| if (strcmp(directory, exclude_lst[i].directory) == 0) |
| return 0; |
| } |
| |
| if (directory == NULL || directory[0] != '/') { |
| selinux_log(SELINUX_ERROR, |
| "Full path required for exclude: %s.\n", |
| directory); |
| errno = EINVAL; |
| return -1; |
| } |
| |
| if (exclude_count >= INT_MAX - 1) { |
| selinux_log(SELINUX_ERROR, "Too many directory excludes: %d.\n", exclude_count); |
| errno = EOVERFLOW; |
| return -1; |
| } |
| |
| tmp_list = realloc(exclude_lst, |
| sizeof(struct edir) * (exclude_count + 1)); |
| if (!tmp_list) |
| goto oom; |
| |
| exclude_lst = tmp_list; |
| |
| len = strlen(directory); |
| while (len > 1 && directory[len - 1] == '/') |
| len--; |
| |
| current = (exclude_lst + exclude_count); |
| |
| current->directory = strndup(directory, len); |
| if (!current->directory) |
| goto oom; |
| |
| current->size = len; |
| current->caller_excluded = who; |
| exclude_count++; |
| return 0; |
| |
| oom: |
| selinux_log(SELINUX_ERROR, "%s: Out of memory\n", __func__); |
| return -1; |
| } |
| |
| static int check_excluded(const char *file) |
| { |
| int i; |
| |
| for (i = 0; i < exclude_count; i++) { |
| if (strncmp(file, exclude_lst[i].directory, |
| exclude_lst[i].size) == 0) { |
| if (file[exclude_lst[i].size] == 0 || |
| file[exclude_lst[i].size] == '/') |
| return 1; |
| } |
| } |
| return 0; |
| } |
| |
| static uint64_t file_system_count(const char *name) |
| { |
| struct statvfs statvfs_buf; |
| uint64_t nfile = 0; |
| |
| memset(&statvfs_buf, 0, sizeof(statvfs_buf)); |
| if (!statvfs(name, &statvfs_buf)) |
| nfile = statvfs_buf.f_files - statvfs_buf.f_ffree; |
| |
| return nfile; |
| } |
| |
| /* |
| * This is called once when selinux_restorecon() is first called. |
| * Searches /proc/mounts for all file systems that do not support extended |
| * attributes and adds them to the exclude directory table. File systems |
| * that support security labels have the seclabel option, return |
| * approximate total file count. |
| */ |
| static uint64_t exclude_non_seclabel_mounts(void) |
| { |
| struct utsname uts; |
| FILE *fp; |
| size_t len; |
| int index = 0, found = 0; |
| uint64_t nfile = 0; |
| char *mount_info[4]; |
| char *buf = NULL, *item; |
| |
| /* Check to see if the kernel supports seclabel */ |
| if (uname(&uts) == 0 && strverscmp(uts.release, "2.6.30") < 0) |
| return 0; |
| if (is_selinux_enabled() <= 0) |
| return 0; |
| |
| fp = fopen("/proc/mounts", "re"); |
| if (!fp) |
| return 0; |
| |
| while (getline(&buf, &len, fp) != -1) { |
| found = 0; |
| index = 0; |
| item = strtok(buf, " "); |
| while (item != NULL) { |
| mount_info[index] = item; |
| index++; |
| if (index == 4) |
| break; |
| item = strtok(NULL, " "); |
| } |
| if (index < 4) { |
| selinux_log(SELINUX_ERROR, |
| "/proc/mounts record \"%s\" has incorrect format.\n", |
| buf); |
| continue; |
| } |
| |
| /* Remove pre-existing entry */ |
| remove_exclude(mount_info[1]); |
| |
| item = strtok(mount_info[3], ","); |
| while (item != NULL) { |
| if (strcmp(item, "seclabel") == 0) { |
| found = 1; |
| nfile += file_system_count(mount_info[1]); |
| break; |
| } |
| item = strtok(NULL, ","); |
| } |
| |
| /* Exclude mount points without the seclabel option */ |
| if (!found) { |
| if (add_exclude(mount_info[1], !CALLER_EXCLUDED) && |
| errno == ENOMEM) |
| assert(0); |
| } |
| } |
| |
| free(buf); |
| fclose(fp); |
| /* return estimated #Files + 5% for directories and hard links */ |
| return nfile * 1.05; |
| } |
| |
| /* Called by selinux_restorecon_xattr(3) to build a linked list of entries. */ |
| static int add_xattr_entry(const char *directory, bool delete_nonmatch, |
| bool delete_all) |
| { |
| char *sha1_buf = NULL; |
| size_t i, digest_len = 0; |
| int rc; |
| enum digest_result digest_result; |
| bool match; |
| struct dir_xattr *new_entry; |
| uint8_t *xattr_digest = NULL; |
| uint8_t *calculated_digest = NULL; |
| |
| if (!directory) { |
| errno = EINVAL; |
| return -1; |
| } |
| |
| match = selabel_get_digests_all_partial_matches(fc_sehandle, directory, |
| &calculated_digest, &xattr_digest, |
| &digest_len); |
| |
| if (!xattr_digest || !digest_len) { |
| free(calculated_digest); |
| return 1; |
| } |
| |
| /* Convert entry to a hex encoded string. */ |
| sha1_buf = malloc(digest_len * 2 + 1); |
| if (!sha1_buf) { |
| free(xattr_digest); |
| free(calculated_digest); |
| goto oom; |
| } |
| |
| for (i = 0; i < digest_len; i++) |
| sprintf((&sha1_buf[i * 2]), "%02x", xattr_digest[i]); |
| |
| digest_result = match ? MATCH : NOMATCH; |
| |
| if ((delete_nonmatch && !match) || delete_all) { |
| digest_result = match ? DELETED_MATCH : DELETED_NOMATCH; |
| rc = removexattr(directory, RESTORECON_PARTIAL_MATCH_DIGEST); |
| if (rc) { |
| selinux_log(SELINUX_ERROR, |
| "Error: %m removing xattr \"%s\" from: %s\n", |
| RESTORECON_PARTIAL_MATCH_DIGEST, directory); |
| digest_result = ERROR; |
| } |
| } |
| free(xattr_digest); |
| free(calculated_digest); |
| |
| /* Now add entries to link list. */ |
| new_entry = malloc(sizeof(struct dir_xattr)); |
| if (!new_entry) { |
| free(sha1_buf); |
| goto oom; |
| } |
| new_entry->next = NULL; |
| |
| new_entry->directory = strdup(directory); |
| if (!new_entry->directory) { |
| free(new_entry); |
| free(sha1_buf); |
| goto oom; |
| } |
| |
| new_entry->digest = strdup(sha1_buf); |
| if (!new_entry->digest) { |
| free(new_entry->directory); |
| free(new_entry); |
| free(sha1_buf); |
| goto oom; |
| } |
| |
| new_entry->result = digest_result; |
| |
| if (!dir_xattr_list) { |
| dir_xattr_list = new_entry; |
| dir_xattr_last = new_entry; |
| } else { |
| dir_xattr_last->next = new_entry; |
| dir_xattr_last = new_entry; |
| } |
| |
| free(sha1_buf); |
| return 0; |
| |
| oom: |
| selinux_log(SELINUX_ERROR, "%s: Out of memory\n", __func__); |
| return -1; |
| } |
| |
| /* |
| * Support filespec services filespec_add(), filespec_eval() and |
| * filespec_destroy(). |
| * |
| * selinux_restorecon(3) uses filespec services when the |
| * SELINUX_RESTORECON_ADD_ASSOC flag is set for adding associations between |
| * an inode and a specification. |
| */ |
| |
| /* |
| * The hash table of associations, hashed by inode number. Chaining is used |
| * for collisions, with elements ordered by inode number in each bucket. |
| * Each hash bucket has a dummy header. |
| */ |
| #define HASH_BITS 16 |
| #define HASH_BUCKETS (1 << HASH_BITS) |
| #define HASH_MASK (HASH_BUCKETS-1) |
| |
| /* |
| * An association between an inode and a context. |
| */ |
| typedef struct file_spec { |
| ino_t ino; /* inode number */ |
| char *con; /* matched context */ |
| char *file; /* full pathname */ |
| struct file_spec *next; /* next association in hash bucket chain */ |
| } file_spec_t; |
| |
| static file_spec_t *fl_head; |
| static pthread_mutex_t fl_mutex = PTHREAD_MUTEX_INITIALIZER; |
| |
| /* |
| * Try to add an association between an inode and a context. If there is a |
| * different context that matched the inode, then use the first context |
| * that matched. |
| */ |
| static int filespec_add(ino_t ino, const char *con, const char *file, |
| const struct rest_flags *flags) |
| { |
| file_spec_t *prevfl, *fl; |
| uint32_t h; |
| int ret; |
| struct stat64 sb; |
| |
| __pthread_mutex_lock(&fl_mutex); |
| |
| if (!fl_head) { |
| fl_head = calloc(HASH_BUCKETS, sizeof(file_spec_t)); |
| if (!fl_head) |
| goto oom; |
| } |
| |
| h = (ino + (ino >> HASH_BITS)) & HASH_MASK; |
| for (prevfl = &fl_head[h], fl = fl_head[h].next; fl; |
| prevfl = fl, fl = fl->next) { |
| if (ino == fl->ino) { |
| ret = lstat64(fl->file, &sb); |
| if (ret < 0 || sb.st_ino != ino) { |
| freecon(fl->con); |
| free(fl->file); |
| fl->file = strdup(file); |
| if (!fl->file) |
| goto oom; |
| fl->con = strdup(con); |
| if (!fl->con) |
| goto oom; |
| goto unlock_1; |
| } |
| |
| if (strcmp(fl->con, con) == 0) |
| goto unlock_1; |
| |
| selinux_log(SELINUX_ERROR, |
| "conflicting specifications for %s and %s, using %s.\n", |
| file, fl->file, fl->con); |
| free(fl->file); |
| fl->file = strdup(file); |
| if (!fl->file) |
| goto oom; |
| |
| __pthread_mutex_unlock(&fl_mutex); |
| |
| if (flags->conflicterror) { |
| selinux_log(SELINUX_ERROR, |
| "treating conflicting specifications as an error.\n"); |
| return -1; |
| } |
| return 1; |
| } |
| |
| if (ino > fl->ino) |
| break; |
| } |
| |
| fl = malloc(sizeof(file_spec_t)); |
| if (!fl) |
| goto oom; |
| fl->ino = ino; |
| fl->con = strdup(con); |
| if (!fl->con) |
| goto oom_freefl; |
| fl->file = strdup(file); |
| if (!fl->file) |
| goto oom_freeflcon; |
| fl->next = prevfl->next; |
| prevfl->next = fl; |
| |
| __pthread_mutex_unlock(&fl_mutex); |
| return 0; |
| |
| oom_freeflcon: |
| free(fl->con); |
| oom_freefl: |
| free(fl); |
| oom: |
| __pthread_mutex_unlock(&fl_mutex); |
| selinux_log(SELINUX_ERROR, "%s: Out of memory\n", __func__); |
| return -1; |
| unlock_1: |
| __pthread_mutex_unlock(&fl_mutex); |
| return 1; |
| } |
| |
| /* |
| * Evaluate the association hash table distribution. |
| */ |
| #ifdef DEBUG |
| static void filespec_eval(void) |
| { |
| file_spec_t *fl; |
| uint32_t h; |
| size_t used, nel, len, longest; |
| |
| if (!fl_head) |
| return; |
| |
| used = 0; |
| longest = 0; |
| nel = 0; |
| for (h = 0; h < HASH_BUCKETS; h++) { |
| len = 0; |
| for (fl = fl_head[h].next; fl; fl = fl->next) |
| len++; |
| if (len) |
| used++; |
| if (len > longest) |
| longest = len; |
| nel += len; |
| } |
| |
| selinux_log(SELINUX_INFO, |
| "filespec hash table stats: %zu elements, %zu/%zu buckets used, longest chain length %zu\n", |
| nel, used, HASH_BUCKETS, longest); |
| } |
| #else |
| static void filespec_eval(void) |
| { |
| } |
| #endif |
| |
| /* |
| * Destroy the association hash table. |
| */ |
| static void filespec_destroy(void) |
| { |
| file_spec_t *fl, *tmp; |
| uint32_t h; |
| |
| if (!fl_head) |
| return; |
| |
| for (h = 0; h < HASH_BUCKETS; h++) { |
| fl = fl_head[h].next; |
| while (fl) { |
| tmp = fl; |
| fl = fl->next; |
| freecon(tmp->con); |
| free(tmp->file); |
| free(tmp); |
| } |
| fl_head[h].next = NULL; |
| } |
| free(fl_head); |
| fl_head = NULL; |
| } |
| |
| /* |
| * Called if SELINUX_RESTORECON_SET_SPECFILE_CTX is not set to check if |
| * the type components differ, updating newtypecon if so. |
| */ |
| static int compare_types(const char *curcon, const char *newcon, char **newtypecon) |
| { |
| int types_differ = 0; |
| context_t cona; |
| context_t conb; |
| int rc = 0; |
| |
| cona = context_new(curcon); |
| if (!cona) { |
| rc = -1; |
| goto out; |
| } |
| conb = context_new(newcon); |
| if (!conb) { |
| context_free(cona); |
| rc = -1; |
| goto out; |
| } |
| |
| types_differ = strcmp(context_type_get(cona), context_type_get(conb)); |
| if (types_differ) { |
| rc |= context_user_set(conb, context_user_get(cona)); |
| rc |= context_role_set(conb, context_role_get(cona)); |
| rc |= context_range_set(conb, context_range_get(cona)); |
| if (!rc) { |
| *newtypecon = strdup(context_str(conb)); |
| if (!*newtypecon) { |
| rc = -1; |
| goto err; |
| } |
| } |
| } |
| |
| err: |
| context_free(cona); |
| context_free(conb); |
| out: |
| return rc; |
| } |
| |
| static int restorecon_sb(const char *pathname, const struct stat *sb, |
| const struct rest_flags *flags, bool first) |
| { |
| char *newcon = NULL; |
| char *curcon = NULL; |
| char *newtypecon = NULL; |
| int rc; |
| const char *lookup_path = pathname; |
| |
| if (rootpath) { |
| if (strncmp(rootpath, lookup_path, rootpathlen) != 0) { |
| selinux_log(SELINUX_ERROR, |
| "%s is not located in alt_rootpath %s\n", |
| lookup_path, rootpath); |
| return -1; |
| } |
| lookup_path += rootpathlen; |
| } |
| |
| if (rootpath != NULL && lookup_path[0] == '\0') |
| /* this is actually the root dir of the alt root. */ |
| rc = selabel_lookup_raw(fc_sehandle, &newcon, "/", |
| sb->st_mode & S_IFMT); |
| else |
| rc = selabel_lookup_raw(fc_sehandle, &newcon, lookup_path, |
| sb->st_mode & S_IFMT); |
| |
| if (rc < 0) { |
| if (errno == ENOENT) { |
| if (flags->warnonnomatch && first) |
| selinux_log(SELINUX_INFO, |
| "Warning no default label for %s\n", |
| lookup_path); |
| |
| return 0; /* no match, but not an error */ |
| } |
| |
| return -1; |
| } |
| |
| if (flags->progress) { |
| __pthread_mutex_lock(&progress_mutex); |
| fc_count++; |
| if (fc_count % STAR_COUNT == 0) { |
| if (flags->mass_relabel && efile_count > 0) { |
| float pc = (fc_count < efile_count) ? (100.0 * |
| fc_count / efile_count) : 100; |
| fprintf(stdout, "\r%-.1f%%", (double)pc); |
| } else { |
| fprintf(stdout, "\r%" PRIu64 "k", fc_count / STAR_COUNT); |
| } |
| fflush(stdout); |
| } |
| __pthread_mutex_unlock(&progress_mutex); |
| } |
| |
| if (flags->add_assoc) { |
| rc = filespec_add(sb->st_ino, newcon, pathname, flags); |
| |
| if (rc < 0) { |
| selinux_log(SELINUX_ERROR, |
| "filespec_add error: %s\n", pathname); |
| freecon(newcon); |
| return -1; |
| } |
| |
| if (rc > 0) { |
| /* Already an association and it took precedence. */ |
| freecon(newcon); |
| return 0; |
| } |
| } |
| |
| if (flags->log_matches) |
| selinux_log(SELINUX_INFO, "%s matched by %s\n", |
| pathname, newcon); |
| |
| if (lgetfilecon_raw(pathname, &curcon) < 0) { |
| if (errno != ENODATA) |
| goto err; |
| |
| curcon = NULL; |
| } |
| |
| if (curcon == NULL || strcmp(curcon, newcon) != 0) { |
| bool updated = false; |
| |
| if (!flags->set_specctx && curcon && |
| (is_context_customizable(curcon) > 0)) { |
| if (flags->verbose) { |
| selinux_log(SELINUX_INFO, |
| "%s not reset as customized by admin to %s\n", |
| pathname, curcon); |
| } |
| goto out; |
| } |
| |
| if (!flags->set_specctx && curcon) { |
| /* If types different then update newcon. */ |
| rc = compare_types(curcon, newcon, &newtypecon); |
| if (rc) |
| goto err; |
| |
| if (newtypecon) { |
| freecon(newcon); |
| newcon = newtypecon; |
| } else { |
| goto out; |
| } |
| } |
| |
| if (!flags->nochange) { |
| if (lsetfilecon(pathname, newcon) < 0) |
| goto err; |
| updated = true; |
| } |
| |
| if (flags->verbose) |
| selinux_log(SELINUX_INFO, |
| "%s %s from %s to %s\n", |
| updated ? "Relabeled" : "Would relabel", |
| pathname, |
| curcon ? curcon : "<no context>", |
| newcon); |
| |
| if (flags->syslog_changes && !flags->nochange) { |
| if (curcon) |
| syslog(LOG_INFO, |
| "relabeling %s from %s to %s\n", |
| pathname, curcon, newcon); |
| else |
| syslog(LOG_INFO, "labeling %s to %s\n", |
| pathname, newcon); |
| } |
| } |
| |
| out: |
| rc = 0; |
| out1: |
| freecon(curcon); |
| freecon(newcon); |
| return rc; |
| err: |
| selinux_log(SELINUX_ERROR, |
| "Could not set context for %s: %m\n", |
| pathname); |
| rc = -1; |
| goto out1; |
| } |
| |
| struct dir_hash_node { |
| char *path; |
| uint8_t digest[SHA1_HASH_SIZE]; |
| struct dir_hash_node *next; |
| }; |
| /* |
| * Returns true if the digest of all partial matched contexts is the same as |
| * the one saved by setxattr. Otherwise returns false and constructs a |
| * dir_hash_node with the newly calculated digest. |
| */ |
| static bool check_context_match_for_dir(const char *pathname, |
| struct dir_hash_node **new_node, |
| int error) |
| { |
| bool status; |
| size_t digest_len = 0; |
| uint8_t *read_digest = NULL; |
| uint8_t *calculated_digest = NULL; |
| |
| if (!new_node) |
| return false; |
| |
| *new_node = NULL; |
| |
| /* status = true if digests match, false otherwise. */ |
| status = selabel_get_digests_all_partial_matches(fc_sehandle, pathname, |
| &calculated_digest, |
| &read_digest, |
| &digest_len); |
| |
| if (status) |
| goto free; |
| |
| /* Save digest of all matched contexts for the current directory. */ |
| if (!error && calculated_digest) { |
| *new_node = calloc(1, sizeof(struct dir_hash_node)); |
| |
| if (!*new_node) |
| goto oom; |
| |
| (*new_node)->path = strdup(pathname); |
| |
| if (!(*new_node)->path) { |
| free(*new_node); |
| *new_node = NULL; |
| goto oom; |
| } |
| memcpy((*new_node)->digest, calculated_digest, digest_len); |
| (*new_node)->next = NULL; |
| } |
| |
| free: |
| free(calculated_digest); |
| free(read_digest); |
| return status; |
| |
| oom: |
| selinux_log(SELINUX_ERROR, "%s: Out of memory\n", __func__); |
| goto free; |
| } |
| |
| struct rest_state { |
| struct rest_flags flags; |
| dev_t dev_num; |
| struct statfs sfsb; |
| bool ignore_digest; |
| bool setrestorecondigest; |
| bool parallel; |
| |
| FTS *fts; |
| FTSENT *ftsent_first; |
| struct dir_hash_node *head, *current; |
| bool abort; |
| int error; |
| long unsigned skipped_errors; |
| int saved_errno; |
| pthread_mutex_t mutex; |
| }; |
| |
| static void *selinux_restorecon_thread(void *arg) |
| { |
| struct rest_state *state = arg; |
| FTS *fts = state->fts; |
| FTSENT *ftsent; |
| int error; |
| char ent_path[PATH_MAX]; |
| struct stat ent_st; |
| bool first = false; |
| |
| if (state->parallel) |
| pthread_mutex_lock(&state->mutex); |
| |
| if (state->ftsent_first) { |
| ftsent = state->ftsent_first; |
| state->ftsent_first = NULL; |
| first = true; |
| goto loop_body; |
| } |
| |
| while (((void)(errno = 0), ftsent = fts_read(fts)) != NULL) { |
| loop_body: |
| /* If the FTS_XDEV flag is set and the device is different */ |
| if (state->flags.set_xdev && |
| ftsent->fts_statp->st_dev != state->dev_num) |
| continue; |
| |
| switch (ftsent->fts_info) { |
| case FTS_DC: |
| selinux_log(SELINUX_ERROR, |
| "Directory cycle on %s.\n", |
| ftsent->fts_path); |
| errno = ELOOP; |
| state->error = -1; |
| state->abort = true; |
| goto finish; |
| case FTS_DP: |
| continue; |
| case FTS_DNR: |
| error = errno; |
| errno = ftsent->fts_errno; |
| selinux_log(SELINUX_ERROR, |
| "Could not read %s: %m.\n", |
| ftsent->fts_path); |
| errno = error; |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| case FTS_NS: |
| error = errno; |
| errno = ftsent->fts_errno; |
| selinux_log(SELINUX_ERROR, |
| "Could not stat %s: %m.\n", |
| ftsent->fts_path); |
| errno = error; |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| case FTS_ERR: |
| error = errno; |
| errno = ftsent->fts_errno; |
| selinux_log(SELINUX_ERROR, |
| "Error on %s: %m.\n", |
| ftsent->fts_path); |
| errno = error; |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| case FTS_D: |
| if (state->sfsb.f_type == SYSFS_MAGIC && |
| !selabel_partial_match(fc_sehandle, |
| ftsent->fts_path)) { |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| } |
| |
| if (check_excluded(ftsent->fts_path)) { |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| } |
| |
| if (state->setrestorecondigest) { |
| struct dir_hash_node *new_node = NULL; |
| |
| if (check_context_match_for_dir(ftsent->fts_path, |
| &new_node, |
| state->error) && |
| !state->ignore_digest) { |
| selinux_log(SELINUX_INFO, |
| "Skipping restorecon on directory(%s)\n", |
| ftsent->fts_path); |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| } |
| |
| if (new_node && !state->error) { |
| if (!state->current) { |
| state->current = new_node; |
| state->head = state->current; |
| } else { |
| state->current->next = new_node; |
| state->current = new_node; |
| } |
| } |
| } |
| /* fall through */ |
| default: |
| if (strlcpy(ent_path, ftsent->fts_path, sizeof(ent_path)) >= sizeof(ent_path)) { |
| selinux_log(SELINUX_ERROR, |
| "Path name too long on %s.\n", |
| ftsent->fts_path); |
| errno = ENAMETOOLONG; |
| state->error = -1; |
| state->abort = true; |
| goto finish; |
| } |
| |
| ent_st = *ftsent->fts_statp; |
| if (state->parallel) |
| pthread_mutex_unlock(&state->mutex); |
| |
| error = restorecon_sb(ent_path, &ent_st, &state->flags, |
| first); |
| |
| if (state->parallel) { |
| pthread_mutex_lock(&state->mutex); |
| if (state->abort) |
| goto unlock; |
| } |
| |
| first = false; |
| if (error) { |
| if (state->flags.abort_on_error) { |
| state->error = error; |
| state->abort = true; |
| goto finish; |
| } |
| if (state->flags.count_errors) |
| state->skipped_errors++; |
| else |
| state->error = error; |
| } |
| break; |
| } |
| } |
| |
| finish: |
| if (!state->saved_errno) |
| state->saved_errno = errno; |
| unlock: |
| if (state->parallel) |
| pthread_mutex_unlock(&state->mutex); |
| return NULL; |
| } |
| |
| static int selinux_restorecon_common(const char *pathname_orig, |
| unsigned int restorecon_flags, |
| size_t nthreads) |
| { |
| struct rest_state state; |
| |
| state.flags.nochange = (restorecon_flags & |
| SELINUX_RESTORECON_NOCHANGE) ? true : false; |
| state.flags.verbose = (restorecon_flags & |
| SELINUX_RESTORECON_VERBOSE) ? true : false; |
| state.flags.progress = (restorecon_flags & |
| SELINUX_RESTORECON_PROGRESS) ? true : false; |
| state.flags.mass_relabel = (restorecon_flags & |
| SELINUX_RESTORECON_MASS_RELABEL) ? true : false; |
| state.flags.recurse = (restorecon_flags & |
| SELINUX_RESTORECON_RECURSE) ? true : false; |
| state.flags.set_specctx = (restorecon_flags & |
| SELINUX_RESTORECON_SET_SPECFILE_CTX) ? true : false; |
| state.flags.userealpath = (restorecon_flags & |
| SELINUX_RESTORECON_REALPATH) ? true : false; |
| state.flags.set_xdev = (restorecon_flags & |
| SELINUX_RESTORECON_XDEV) ? true : false; |
| state.flags.add_assoc = (restorecon_flags & |
| SELINUX_RESTORECON_ADD_ASSOC) ? true : false; |
| state.flags.abort_on_error = (restorecon_flags & |
| SELINUX_RESTORECON_ABORT_ON_ERROR) ? true : false; |
| state.flags.syslog_changes = (restorecon_flags & |
| SELINUX_RESTORECON_SYSLOG_CHANGES) ? true : false; |
| state.flags.log_matches = (restorecon_flags & |
| SELINUX_RESTORECON_LOG_MATCHES) ? true : false; |
| state.flags.ignore_noent = (restorecon_flags & |
| SELINUX_RESTORECON_IGNORE_NOENTRY) ? true : false; |
| state.flags.warnonnomatch = true; |
| state.flags.conflicterror = (restorecon_flags & |
| SELINUX_RESTORECON_CONFLICT_ERROR) ? true : false; |
| ignore_mounts = (restorecon_flags & |
| SELINUX_RESTORECON_IGNORE_MOUNTS) ? true : false; |
| state.ignore_digest = (restorecon_flags & |
| SELINUX_RESTORECON_IGNORE_DIGEST) ? true : false; |
| state.flags.count_errors = (restorecon_flags & |
| SELINUX_RESTORECON_COUNT_ERRORS) ? true : false; |
| state.setrestorecondigest = true; |
| |
| state.head = NULL; |
| state.current = NULL; |
| state.abort = false; |
| state.error = 0; |
| state.skipped_errors = 0; |
| state.saved_errno = 0; |
| |
| struct stat sb; |
| char *pathname = NULL, *pathdnamer = NULL, *pathdname, *pathbname; |
| char *paths[2] = { NULL, NULL }; |
| int fts_flags, error; |
| struct dir_hash_node *current = NULL; |
| |
| if (state.flags.verbose && state.flags.progress) |
| state.flags.verbose = false; |
| |
| __selinux_once(fc_once, restorecon_init); |
| |
| if (!fc_sehandle) |
| return -1; |
| |
| /* |
| * If selabel_no_digest = true then no digest has been requested by |
| * an external selabel_open(3) call. |
| */ |
| if (selabel_no_digest || |
| (restorecon_flags & SELINUX_RESTORECON_SKIP_DIGEST)) |
| state.setrestorecondigest = false; |
| |
| if (!__pthread_supported) { |
| if (nthreads != 1) { |
| nthreads = 1; |
| selinux_log(SELINUX_WARNING, |
| "Threading functionality not available, falling back to 1 thread."); |
| } |
| } else if (nthreads == 0) { |
| long nproc = sysconf(_SC_NPROCESSORS_ONLN); |
| |
| if (nproc > 0) { |
| nthreads = nproc; |
| } else { |
| nthreads = 1; |
| selinux_log(SELINUX_WARNING, |
| "Unable to detect CPU count, falling back to 1 thread."); |
| } |
| } |
| |
| /* |
| * Convert passed-in pathname to canonical pathname by resolving |
| * realpath of containing dir, then appending last component name. |
| */ |
| if (state.flags.userealpath) { |
| char *basename_cpy = strdup(pathname_orig); |
| if (!basename_cpy) |
| goto realpatherr; |
| pathbname = basename(basename_cpy); |
| if (!strcmp(pathbname, "/") || !strcmp(pathbname, ".") || |
| !strcmp(pathbname, "..")) { |
| pathname = realpath(pathname_orig, NULL); |
| if (!pathname) { |
| free(basename_cpy); |
| /* missing parent directory */ |
| if (state.flags.ignore_noent && errno == ENOENT) { |
| return 0; |
| } |
| goto realpatherr; |
| } |
| } else { |
| char *dirname_cpy = strdup(pathname_orig); |
| if (!dirname_cpy) { |
| free(basename_cpy); |
| goto realpatherr; |
| } |
| pathdname = dirname(dirname_cpy); |
| pathdnamer = realpath(pathdname, NULL); |
| free(dirname_cpy); |
| if (!pathdnamer) { |
| free(basename_cpy); |
| if (state.flags.ignore_noent && errno == ENOENT) { |
| return 0; |
| } |
| goto realpatherr; |
| } |
| if (!strcmp(pathdnamer, "/")) |
| error = asprintf(&pathname, "/%s", pathbname); |
| else |
| error = asprintf(&pathname, "%s/%s", |
| pathdnamer, pathbname); |
| if (error < 0) { |
| free(basename_cpy); |
| goto oom; |
| } |
| } |
| free(basename_cpy); |
| } else { |
| pathname = strdup(pathname_orig); |
| if (!pathname) |
| goto oom; |
| } |
| |
| paths[0] = pathname; |
| |
| if (lstat(pathname, &sb) < 0) { |
| if (state.flags.ignore_noent && errno == ENOENT) { |
| free(pathdnamer); |
| free(pathname); |
| return 0; |
| } else { |
| selinux_log(SELINUX_ERROR, |
| "lstat(%s) failed: %m\n", |
| pathname); |
| error = -1; |
| goto cleanup; |
| } |
| } |
| |
| /* Skip digest if not a directory */ |
| if (!S_ISDIR(sb.st_mode)) |
| state.setrestorecondigest = false; |
| |
| if (!state.flags.recurse) { |
| if (check_excluded(pathname)) { |
| error = 0; |
| goto cleanup; |
| } |
| |
| error = restorecon_sb(pathname, &sb, &state.flags, true); |
| goto cleanup; |
| } |
| |
| /* Obtain fs type */ |
| memset(&state.sfsb, 0, sizeof(state.sfsb)); |
| if (!S_ISLNK(sb.st_mode) && statfs(pathname, &state.sfsb) < 0) { |
| selinux_log(SELINUX_ERROR, |
| "statfs(%s) failed: %m\n", |
| pathname); |
| error = -1; |
| goto cleanup; |
| } |
| |
| /* Skip digest on in-memory filesystems and /sys */ |
| if (state.sfsb.f_type == RAMFS_MAGIC || state.sfsb.f_type == TMPFS_MAGIC || |
| state.sfsb.f_type == SYSFS_MAGIC) |
| state.setrestorecondigest = false; |
| |
| if (state.flags.set_xdev) |
| fts_flags = FTS_PHYSICAL | FTS_NOCHDIR | FTS_XDEV; |
| else |
| fts_flags = FTS_PHYSICAL | FTS_NOCHDIR; |
| |
| state.fts = fts_open(paths, fts_flags, NULL); |
| if (!state.fts) |
| goto fts_err; |
| |
| state.ftsent_first = fts_read(state.fts); |
| if (!state.ftsent_first) |
| goto fts_err; |
| |
| /* |
| * Keep the inode of the first device. This is because the FTS_XDEV |
| * flag tells fts not to descend into directories with different |
| * device numbers, but fts will still give back the actual directory. |
| * By saving the device number of the directory that was passed to |
| * selinux_restorecon() and then skipping all actions on any |
| * directories with a different device number when the FTS_XDEV flag |
| * is set (from http://marc.info/?l=selinux&m=124688830500777&w=2). |
| */ |
| state.dev_num = state.ftsent_first->fts_statp->st_dev; |
| |
| if (nthreads == 1) { |
| state.parallel = false; |
| selinux_restorecon_thread(&state); |
| } else { |
| size_t i; |
| pthread_t self = pthread_self(); |
| pthread_t *threads = NULL; |
| |
| pthread_mutex_init(&state.mutex, NULL); |
| |
| threads = calloc(nthreads - 1, sizeof(*threads)); |
| if (!threads) |
| goto oom; |
| |
| state.parallel = true; |
| /* |
| * Start (nthreads - 1) threads - the main thread is going to |
| * take part, too. |
| */ |
| for (i = 0; i < nthreads - 1; i++) { |
| if (pthread_create(&threads[i], NULL, |
| selinux_restorecon_thread, &state)) { |
| /* |
| * If any thread fails to be created, just mark |
| * it as such and let the successfully created |
| * threads do the job. In the worst case the |
| * main thread will do everything, but that's |
| * still better than to give up. |
| */ |
| threads[i] = self; |
| } |
| } |
| |
| /* Let's join in on the fun! */ |
| selinux_restorecon_thread(&state); |
| |
| /* Now wait for all threads to finish. */ |
| for (i = 0; i < nthreads - 1; i++) { |
| /* Skip threads that failed to be created. */ |
| if (pthread_equal(threads[i], self)) |
| continue; |
| pthread_join(threads[i], NULL); |
| } |
| free(threads); |
| |
| pthread_mutex_destroy(&state.mutex); |
| } |
| |
| error = state.error; |
| if (state.saved_errno) |
| goto out; |
| |
| /* |
| * Labeling successful. Write partial match digests for subdirectories. |
| * TODO: Write digest upon FTS_DP if no error occurs in its descents. |
| * Note: we can't ignore errors here that we've masked due to |
| * SELINUX_RESTORECON_COUNT_ERRORS. |
| */ |
| if (state.setrestorecondigest && !state.flags.nochange && !error && |
| state.skipped_errors == 0) { |
| current = state.head; |
| while (current != NULL) { |
| if (setxattr(current->path, |
| RESTORECON_PARTIAL_MATCH_DIGEST, |
| current->digest, |
| SHA1_HASH_SIZE, 0) < 0) { |
| selinux_log(SELINUX_ERROR, |
| "setxattr failed: %s: %m\n", |
| current->path); |
| } |
| current = current->next; |
| } |
| } |
| |
| skipped_errors = state.skipped_errors; |
| |
| out: |
| if (state.flags.progress && state.flags.mass_relabel) |
| fprintf(stdout, "\r%s 100.0%%\n", pathname); |
| |
| (void) fts_close(state.fts); |
| errno = state.saved_errno; |
| cleanup: |
| if (state.flags.add_assoc) { |
| if (state.flags.verbose) |
| filespec_eval(); |
| filespec_destroy(); |
| } |
| free(pathdnamer); |
| free(pathname); |
| |
| current = state.head; |
| while (current != NULL) { |
| struct dir_hash_node *next = current->next; |
| |
| free(current->path); |
| free(current); |
| current = next; |
| } |
| return error; |
| |
| oom: |
| selinux_log(SELINUX_ERROR, "%s: Out of memory\n", __func__); |
| error = -1; |
| goto cleanup; |
| |
| realpatherr: |
| selinux_log(SELINUX_ERROR, |
| "SELinux: Could not get canonical path for %s restorecon: %m.\n", |
| pathname_orig); |
| error = -1; |
| goto cleanup; |
| |
| fts_err: |
| selinux_log(SELINUX_ERROR, |
| "fts error while labeling %s: %m\n", |
| paths[0]); |
| error = -1; |
| goto cleanup; |
| } |
| |
| |
| /* |
| * Public API |
| */ |
| |
| /* selinux_restorecon(3) - Main function that is responsible for labeling */ |
| int selinux_restorecon(const char *pathname_orig, |
| unsigned int restorecon_flags) |
| { |
| return selinux_restorecon_common(pathname_orig, restorecon_flags, 1); |
| } |
| |
| /* selinux_restorecon_parallel(3) - Parallel version of selinux_restorecon(3) */ |
| int selinux_restorecon_parallel(const char *pathname_orig, |
| unsigned int restorecon_flags, |
| size_t nthreads) |
| { |
| return selinux_restorecon_common(pathname_orig, restorecon_flags, nthreads); |
| } |
| |
| /* selinux_restorecon_set_sehandle(3) is called to set the global fc handle */ |
| void selinux_restorecon_set_sehandle(struct selabel_handle *hndl) |
| { |
| char **specfiles; |
| unsigned char *fc_digest; |
| size_t num_specfiles, fc_digest_len; |
| |
| fc_sehandle = hndl; |
| if (!fc_sehandle) |
| return; |
| |
| /* Check if digest requested in selabel_open(3), if so use it. */ |
| if (selabel_digest(fc_sehandle, &fc_digest, &fc_digest_len, |
| &specfiles, &num_specfiles) < 0) |
| selabel_no_digest = true; |
| else |
| selabel_no_digest = false; |
| } |
| |
| |
| /* |
| * selinux_restorecon_default_handle(3) is called to set the global restorecon |
| * handle by a process if the default params are required. |
| */ |
| struct selabel_handle *selinux_restorecon_default_handle(void) |
| { |
| struct selabel_handle *sehandle; |
| |
| struct selinux_opt fc_opts[] = { |
| { SELABEL_OPT_DIGEST, (char *)1 } |
| }; |
| |
| sehandle = selabel_open(SELABEL_CTX_FILE, fc_opts, 1); |
| |
| if (!sehandle) { |
| selinux_log(SELINUX_ERROR, |
| "Error obtaining file context handle: %m\n"); |
| return NULL; |
| } |
| |
| selabel_no_digest = false; |
| return sehandle; |
| } |
| |
| /* |
| * selinux_restorecon_set_exclude_list(3) is called to add additional entries |
| * to be excluded from labeling checks. |
| */ |
| void selinux_restorecon_set_exclude_list(const char **exclude_list) |
| { |
| int i; |
| struct stat sb; |
| |
| for (i = 0; exclude_list[i]; i++) { |
| if (lstat(exclude_list[i], &sb) < 0 && errno != EACCES) { |
| selinux_log(SELINUX_ERROR, |
| "lstat error on exclude path \"%s\", %m - ignoring.\n", |
| exclude_list[i]); |
| break; |
| } |
| if (add_exclude(exclude_list[i], CALLER_EXCLUDED) && |
| errno == ENOMEM) |
| assert(0); |
| } |
| } |
| |
| /* selinux_restorecon_set_alt_rootpath(3) sets an alternate rootpath. */ |
| int selinux_restorecon_set_alt_rootpath(const char *alt_rootpath) |
| { |
| size_t len; |
| |
| /* This should be NULL on first use */ |
| if (rootpath) |
| free(rootpath); |
| |
| rootpath = strdup(alt_rootpath); |
| if (!rootpath) { |
| selinux_log(SELINUX_ERROR, "%s: Out of memory\n", __func__); |
| return -1; |
| } |
| |
| /* trim trailing /, if present */ |
| len = strlen(rootpath); |
| while (len && (rootpath[len - 1] == '/')) |
| rootpath[--len] = '\0'; |
| rootpathlen = len; |
| |
| return 0; |
| } |
| |
| /* selinux_restorecon_xattr(3) |
| * Find RESTORECON_PARTIAL_MATCH_DIGEST entries. |
| */ |
| int selinux_restorecon_xattr(const char *pathname, unsigned int xattr_flags, |
| struct dir_xattr ***xattr_list) |
| { |
| bool recurse = (xattr_flags & |
| SELINUX_RESTORECON_XATTR_RECURSE) ? true : false; |
| bool delete_nonmatch = (xattr_flags & |
| SELINUX_RESTORECON_XATTR_DELETE_NONMATCH_DIGESTS) ? true : false; |
| bool delete_all = (xattr_flags & |
| SELINUX_RESTORECON_XATTR_DELETE_ALL_DIGESTS) ? true : false; |
| ignore_mounts = (xattr_flags & |
| SELINUX_RESTORECON_XATTR_IGNORE_MOUNTS) ? true : false; |
| |
| int rc, fts_flags; |
| struct stat sb; |
| struct statfs sfsb; |
| struct dir_xattr *current, *next; |
| FTS *fts; |
| FTSENT *ftsent; |
| char *paths[2] = { NULL, NULL }; |
| |
| __selinux_once(fc_once, restorecon_init); |
| |
| if (!fc_sehandle) |
| return -1; |
| |
| if (lstat(pathname, &sb) < 0) { |
| if (errno == ENOENT) |
| return 0; |
| |
| selinux_log(SELINUX_ERROR, |
| "lstat(%s) failed: %m\n", |
| pathname); |
| return -1; |
| } |
| |
| if (!recurse) { |
| if (statfs(pathname, &sfsb) == 0) { |
| if (sfsb.f_type == RAMFS_MAGIC || |
| sfsb.f_type == TMPFS_MAGIC) |
| return 0; |
| } |
| |
| if (check_excluded(pathname)) |
| return 0; |
| |
| rc = add_xattr_entry(pathname, delete_nonmatch, delete_all); |
| |
| if (!rc && dir_xattr_list) |
| *xattr_list = &dir_xattr_list; |
| else if (rc == -1) |
| return rc; |
| |
| return 0; |
| } |
| |
| paths[0] = (char *)pathname; |
| fts_flags = FTS_PHYSICAL | FTS_NOCHDIR; |
| |
| fts = fts_open(paths, fts_flags, NULL); |
| if (!fts) { |
| selinux_log(SELINUX_ERROR, |
| "fts error on %s: %m\n", |
| paths[0]); |
| return -1; |
| } |
| |
| while ((ftsent = fts_read(fts)) != NULL) { |
| switch (ftsent->fts_info) { |
| case FTS_DP: |
| continue; |
| case FTS_D: |
| if (statfs(ftsent->fts_path, &sfsb) == 0) { |
| if (sfsb.f_type == RAMFS_MAGIC || |
| sfsb.f_type == TMPFS_MAGIC) |
| continue; |
| } |
| if (check_excluded(ftsent->fts_path)) { |
| fts_set(fts, ftsent, FTS_SKIP); |
| continue; |
| } |
| |
| rc = add_xattr_entry(ftsent->fts_path, |
| delete_nonmatch, delete_all); |
| if (rc == 1) |
| continue; |
| else if (rc == -1) |
| goto cleanup; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| if (dir_xattr_list) |
| *xattr_list = &dir_xattr_list; |
| |
| (void) fts_close(fts); |
| return 0; |
| |
| cleanup: |
| rc = errno; |
| (void) fts_close(fts); |
| errno = rc; |
| |
| if (dir_xattr_list) { |
| /* Free any used memory */ |
| current = dir_xattr_list; |
| while (current) { |
| next = current->next; |
| free(current->directory); |
| free(current->digest); |
| free(current); |
| current = next; |
| } |
| } |
| return -1; |
| } |
| |
| long unsigned selinux_restorecon_get_skipped_errors(void) |
| { |
| return skipped_errors; |
| } |