busybox/selinux/setfiles.c
James Byrne 6937487be7 libbb: reduce the overhead of single parameter bb_error_msg() calls
Back in 2007, commit 0c97c9d437 ("'simple' error message functions by
Loic Grenie") introduced bb_simple_perror_msg() to allow for a lower
overhead call to bb_perror_msg() when only a string was being printed
with no parameters. This saves space for some CPU architectures because
it avoids the overhead of a call to a variadic function. However there
has never been a simple version of bb_error_msg(), and since 2007 many
new calls to bb_perror_msg() have been added that only take a single
parameter and so could have been using bb_simple_perror_message().

This changeset introduces 'simple' versions of bb_info_msg(),
bb_error_msg(), bb_error_msg_and_die(), bb_herror_msg() and
bb_herror_msg_and_die(), and replaces all calls that only take a
single parameter, or use something like ("%s", arg), with calls to the
corresponding 'simple' version.

Since it is likely that single parameter calls to the variadic functions
may be accidentally reintroduced in the future a new debugging config
option WARN_SIMPLE_MSG has been introduced. This uses some macro magic
which will cause any such calls to generate a warning, but this is
turned off by default to avoid use of the unpleasant macros in normal
circumstances.

This is a large changeset due to the number of calls that have been
replaced. The only files that contain changes other than simple
substitution of function calls are libbb.h, libbb/herror_msg.c,
libbb/verror_msg.c and libbb/xfuncs_printf.c. In miscutils/devfsd.c,
networking/udhcp/common.h and util-linux/mdev.c additonal macros have
been added for logging so that single parameter and multiple parameter
logging variants exist.

The amount of space saved varies considerably by architecture, and was
found to be as follows (for 'defconfig' using GCC 7.4):

Arm:     -92 bytes
MIPS:    -52 bytes
PPC:   -1836 bytes
x86_64: -938 bytes

Note that for the MIPS architecture only an exception had to be made
disabling the 'simple' calls for 'udhcp' (in networking/udhcp/common.h)
because it made these files larger on MIPS.

Signed-off-by: James Byrne <james.byrne@origamienergy.com>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
2019-07-02 11:35:03 +02:00

724 lines
19 KiB
C

/*
setfiles: based on policycoreutils 2.0.19
policycoreutils was released under GPL 2.
Port to BusyBox (c) 2007 by Yuichi Nakamura <ynakam@hitachisoft.jp>
*/
//config:config SETFILES
//config: bool "setfiles (13 kb)"
//config: default n
//config: depends on SELINUX
//config: help
//config: Enable support to modify to relabel files.
//config: Notice: If you built libselinux with -D_FILE_OFFSET_BITS=64,
//config: (It is default in libselinux's Makefile), you _must_ enable
//config: CONFIG_LFS.
//config:
//config:config FEATURE_SETFILES_CHECK_OPTION
//config: bool "Enable check option"
//config: default n
//config: depends on SETFILES
//config: help
//config: Support "-c" option (check the validity of the contexts against
//config: the specified binary policy) for setfiles. Requires libsepol.
//config:
//config:config RESTORECON
//config: bool "restorecon (12 kb)"
//config: default n
//config: depends on SELINUX
//config: help
//config: Enable support to relabel files. The feature is almost
//config: the same as setfiles, but usage is a little different.
//applet:IF_SETFILES(APPLET(setfiles, BB_DIR_SBIN, BB_SUID_DROP))
// APPLET_ODDNAME:name main location suid_type help
//applet:IF_RESTORECON(APPLET_ODDNAME(restorecon, setfiles, BB_DIR_SBIN, BB_SUID_DROP, restorecon))
//kbuild:lib-$(CONFIG_SETFILES) += setfiles.o
//kbuild:lib-$(CONFIG_RESTORECON) += setfiles.o
//usage:#define setfiles_trivial_usage
//usage: "[-dnpqsvW] [-e DIR]... [-o FILE] [-r alt_root_path]"
//usage: IF_FEATURE_SETFILES_CHECK_OPTION(
//usage: " [-c policyfile] spec_file"
//usage: )
//usage: " pathname"
//usage:#define setfiles_full_usage "\n\n"
//usage: "Reset file contexts under pathname according to spec_file\n"
//usage: IF_FEATURE_SETFILES_CHECK_OPTION(
//usage: "\n -c FILE Check the validity of the contexts against the specified binary policy"
//usage: )
//usage: "\n -d Show which specification matched each file"
//usage: "\n -l Log changes in file labels to syslog"
//TODO: log to syslog is not yet implemented, it goes to stdout only now
//usage: "\n -n Don't change any file labels"
//usage: "\n -q Suppress warnings"
//usage: "\n -r DIR Use an alternate root path"
//usage: "\n -e DIR Exclude DIR"
//usage: "\n -F Force reset of context to match file_context for customizable files"
//usage: "\n -o FILE Save list of files with incorrect context"
//usage: "\n -s Take a list of files from stdin (instead of command line)"
//usage: "\n -v Show changes in file labels, if type or role are changing"
//usage: "\n -vv Show changes in file labels, if type, role, or user are changing"
//usage: "\n -W Display warnings about entries that had no matching files"
//usage:
//usage:#define restorecon_trivial_usage
//usage: "[-iFnRv] [-e EXCLUDEDIR]... [-o FILE] [-f FILE]"
//usage:#define restorecon_full_usage "\n\n"
//usage: "Reset security contexts of files in pathname\n"
//usage: "\n -i Ignore files that don't exist"
//usage: "\n -f FILE File with list of files to process"
//usage: "\n -e DIR Directory to exclude"
//usage: "\n -R,-r Recurse"
//usage: "\n -n Don't change any file labels"
//usage: "\n -o FILE Save list of files with incorrect context"
//usage: "\n -v Verbose"
//usage: "\n -vv Show changed labels"
//usage: "\n -F Force reset of context to match file_context"
//usage: "\n for customizable files, or the user section,"
//usage: "\n if it has changed"
#include "libbb.h"
#include "common_bufsiz.h"
#if ENABLE_FEATURE_SETFILES_CHECK_OPTION
#include <sepol/sepol.h>
#endif
#define MAX_EXCLUDES 50
struct edir {
char *directory;
size_t size;
};
struct globals {
FILE *outfile;
char *policyfile;
char *rootpath;
int rootpathlen;
unsigned count;
int excludeCtr;
int errors;
int verbose; /* getopt32 uses it, has to be int */
smallint recurse; /* Recursive descent */
smallint follow_mounts;
/* Behavior flags determined based on setfiles vs. restorecon */
smallint expand_realpath; /* Expand paths via realpath */
smallint abort_on_error; /* Abort the file tree walk upon an error */
int add_assoc; /* Track inode associations for conflict detection */
int matchpathcon_flags; /* Flags to matchpathcon */
dev_t dev_id; /* Device id where target file exists */
int nerr;
struct edir excludeArray[MAX_EXCLUDES];
} FIX_ALIASING;
#define G (*(struct globals*)bb_common_bufsiz1)
void BUG_setfiles_globals_too_big(void);
#define INIT_G() do { \
setup_common_bufsiz(); \
if (sizeof(G) > COMMON_BUFSIZE) \
BUG_setfiles_globals_too_big(); \
/* memset(&G, 0, sizeof(G)); - already is */ \
} while (0)
#define outfile (G.outfile )
#define policyfile (G.policyfile )
#define rootpath (G.rootpath )
#define rootpathlen (G.rootpathlen )
#define count (G.count )
#define excludeCtr (G.excludeCtr )
#define errors (G.errors )
#define verbose (G.verbose )
#define recurse (G.recurse )
#define follow_mounts (G.follow_mounts )
#define expand_realpath (G.expand_realpath )
#define abort_on_error (G.abort_on_error )
#define add_assoc (G.add_assoc )
#define matchpathcon_flags (G.matchpathcon_flags)
#define dev_id (G.dev_id )
#define nerr (G.nerr )
#define excludeArray (G.excludeArray )
/* Must match getopt32 string! */
enum {
OPT_d = (1 << 0),
OPT_e = (1 << 1),
OPT_f = (1 << 2),
OPT_i = (1 << 3),
OPT_l = (1 << 4),
OPT_n = (1 << 5),
OPT_p = (1 << 6),
OPT_q = (1 << 7),
OPT_r = (1 << 8),
OPT_s = (1 << 9),
OPT_v = (1 << 10),
OPT_o = (1 << 11),
OPT_F = (1 << 12),
OPT_W = (1 << 13),
OPT_c = (1 << 14), /* c only for setfiles */
OPT_R = (1 << 14), /* R only for restorecon */
};
#define FLAG_d_debug (option_mask32 & OPT_d)
#define FLAG_e (option_mask32 & OPT_e)
#define FLAG_f (option_mask32 & OPT_f)
#define FLAG_i_ignore_enoent (option_mask32 & OPT_i)
#define FLAG_l_take_log (option_mask32 & OPT_l)
#define FLAG_n_dry_run (option_mask32 & OPT_n)
#define FLAG_p_progress (option_mask32 & OPT_p)
#define FLAG_q_quiet (option_mask32 & OPT_q)
#define FLAG_r (option_mask32 & OPT_r)
#define FLAG_s (option_mask32 & OPT_s)
#define FLAG_v (option_mask32 & OPT_v)
#define FLAG_o (option_mask32 & OPT_o)
#define FLAG_F_force (option_mask32 & OPT_F)
#define FLAG_W_warn_no_match (option_mask32 & OPT_W)
#define FLAG_c (option_mask32 & OPT_c)
#define FLAG_R (option_mask32 & OPT_R)
static void qprintf(const char *fmt UNUSED_PARAM, ...)
{
/* quiet, do nothing */
}
static void inc_err(void)
{
nerr++;
if (nerr > 9 && !FLAG_d_debug) {
bb_simple_error_msg_and_die("exiting after 10 errors");
}
}
static void add_exclude(const char *directory)
{
struct stat sb;
size_t len;
if (directory == NULL || directory[0] != '/') {
bb_error_msg_and_die("full path required for exclude: %s", directory);
}
if (lstat(directory, &sb)) {
bb_error_msg("directory \"%s\" not found, ignoring", directory);
return;
}
if ((sb.st_mode & S_IFDIR) == 0) {
bb_error_msg("\"%s\" is not a directory: mode %o, ignoring",
directory, sb.st_mode);
return;
}
if (excludeCtr == MAX_EXCLUDES) {
bb_error_msg_and_die("maximum excludes %d exceeded", MAX_EXCLUDES);
}
len = strlen(directory);
while (len > 1 && directory[len - 1] == '/') {
len--;
}
excludeArray[excludeCtr].directory = xstrndup(directory, len);
excludeArray[excludeCtr++].size = len;
}
static bool exclude(const char *file)
{
int i = 0;
for (i = 0; i < excludeCtr; i++) {
if (strncmp(file, excludeArray[i].directory,
excludeArray[i].size) == 0) {
if (file[excludeArray[i].size] == '\0'
|| file[excludeArray[i].size] == '/') {
return 1;
}
}
}
return 0;
}
static int match(const char *name, struct stat *sb, char **con)
{
int ret;
char path[PATH_MAX + 1];
char *tmp_path = xstrdup(name);
if (excludeCtr > 0 && exclude(name)) {
goto err;
}
ret = lstat(name, sb);
if (ret) {
if (FLAG_i_ignore_enoent && errno == ENOENT) {
free(tmp_path);
return 0;
}
bb_error_msg("stat(%s)", name);
goto err;
}
if (expand_realpath) {
if (S_ISLNK(sb->st_mode)) {
char *p = NULL;
char *file_sep;
size_t len = 0;
if (verbose > 1)
bb_error_msg("warning! %s refers to a symbolic link, not following last component", name);
file_sep = strrchr(tmp_path, '/');
if (file_sep == tmp_path) {
file_sep++;
path[0] = '\0';
p = path;
} else if (file_sep) {
*file_sep++ = '\0';
p = realpath(tmp_path, path);
} else {
file_sep = tmp_path;
p = realpath("./", path);
}
if (p)
len = strlen(p);
if (!p || len + strlen(file_sep) + 2 > PATH_MAX) {
bb_perror_msg("realpath(%s) failed", name);
goto err;
}
p += len;
/* ensure trailing slash of directory name */
if (len == 0 || p[-1] != '/') {
*p++ = '/';
}
strcpy(p, file_sep);
name = path;
if (excludeCtr > 0 && exclude(name))
goto err;
} else {
char *p;
p = realpath(name, path);
if (!p) {
bb_perror_msg("realpath(%s)", name);
goto err;
}
name = p;
if (excludeCtr > 0 && exclude(name))
goto err;
}
}
/* name will be what is matched in the policy */
if (NULL != rootpath) {
if (0 != strncmp(rootpath, name, rootpathlen)) {
bb_error_msg("%s is not located in %s",
name, rootpath);
goto err;
}
name += rootpathlen;
}
free(tmp_path);
if (rootpath != NULL && name[0] == '\0')
/* this is actually the root dir of the alt root */
return matchpathcon_index("/", sb->st_mode, con);
return matchpathcon_index(name, sb->st_mode, con);
err:
free(tmp_path);
return -1;
}
/* Compare two contexts to see if their differences are "significant",
* or whether the only difference is in the user. */
static bool only_changed_user(const char *a, const char *b)
{
if (FLAG_F_force)
return 0;
if (!a || !b)
return 0;
a = strchr(a, ':'); /* Rest of the context after the user */
b = strchr(b, ':');
if (!a || !b)
return 0;
return (strcmp(a, b) == 0);
}
static int restore(const char *file)
{
char *my_file;
struct stat my_sb;
int i, j, ret;
char *context = NULL;
char *newcon = NULL;
bool user_only_changed = 0;
int retval = 0;
my_file = bb_simplify_path(file);
i = match(my_file, &my_sb, &newcon);
if (i < 0) /* No matching specification. */
goto out;
if (FLAG_p_progress) {
count++;
if (count % 0x400 == 0) { /* every 1024 times */
count = (count % (80*0x400));
if (count == 0)
bb_putchar('\n');
bb_putchar('*');
fflush_all();
}
}
/*
* Try to add an association between this inode and
* this specification. If there is already an association
* for this inode and it conflicts with this specification,
* then use the last matching specification.
*/
if (add_assoc) {
j = matchpathcon_filespec_add(my_sb.st_ino, i, my_file);
if (j < 0)
goto err;
if (j != i) {
/* There was already an association and it took precedence. */
goto out;
}
}
if (FLAG_d_debug)
printf("%s: %s matched by %s\n", applet_name, my_file, newcon);
/* Get the current context of the file. */
ret = lgetfilecon_raw(my_file, &context);
if (ret < 0) {
if (errno == ENODATA) {
context = NULL; /* paranoia */
} else {
bb_perror_msg("lgetfilecon_raw on %s", my_file);
goto err;
}
user_only_changed = 0;
} else
user_only_changed = only_changed_user(context, newcon);
/*
* Do not relabel the file if the matching specification is
* <<none>> or the file is already labeled according to the
* specification.
*/
if ((strcmp(newcon, "<<none>>") == 0)
|| (context && (strcmp(context, newcon) == 0) && !FLAG_F_force)) {
goto out;
}
if (!FLAG_F_force && context && (is_context_customizable(context) > 0)) {
if (verbose > 1) {
bb_error_msg("skipping %s. %s is customizable_types",
my_file, context);
}
goto out;
}
if (verbose) {
/* If we're just doing "-v", trim out any relabels where
* the user has changed but the role and type are the
* same. For "-vv", emit everything. */
if (verbose > 1 || !user_only_changed) {
printf("%s: reset %s context %s->%s\n",
applet_name, my_file, context ? context : "", newcon);
}
}
if (FLAG_l_take_log && !user_only_changed) {
if (context)
printf("relabeling %s from %s to %s\n", my_file, context, newcon);
else
printf("labeling %s to %s\n", my_file, newcon);
}
if (outfile && !user_only_changed)
fprintf(outfile, "%s\n", my_file);
/*
* Do not relabel the file if -n was used.
*/
if (FLAG_n_dry_run || user_only_changed)
goto out;
/*
* Relabel the file to the specified context.
*/
ret = lsetfilecon(my_file, newcon);
if (ret) {
bb_perror_msg("lsetfileconon(%s,%s)", my_file, newcon);
goto err;
}
out:
freecon(context);
freecon(newcon);
free(my_file);
return retval;
err:
retval--; /* -1 */
goto out;
}
/*
* Apply the last matching specification to a file.
* This function is called by recursive_action on each file during
* the directory traversal.
*/
static int FAST_FUNC apply_spec(
const char *file,
struct stat *sb,
void *userData UNUSED_PARAM,
int depth UNUSED_PARAM)
{
if (!follow_mounts) {
/* setfiles does not process across different mount points */
if (sb->st_dev != dev_id) {
return SKIP;
}
}
errors |= restore(file);
if (abort_on_error && errors)
return FALSE;
return TRUE;
}
static int canoncon(const char *path, unsigned lineno, char **contextp)
{
static const char err_msg[] ALIGN1 = "%s: line %u has invalid context %s";
char *tmpcon;
char *context = *contextp;
int invalid = 0;
#if ENABLE_FEATURE_SETFILES_CHECK_OPTION
if (policyfile) {
if (sepol_check_context(context) >= 0)
return 0;
/* Exit immediately if we're in checking mode. */
bb_error_msg_and_die(err_msg, path, lineno, context);
}
#endif
if (security_canonicalize_context_raw(context, &tmpcon) < 0) {
if (errno != ENOENT) {
invalid = 1;
inc_err();
}
} else {
free(context);
*contextp = tmpcon;
}
if (invalid) {
bb_error_msg(err_msg, path, lineno, context);
}
return invalid;
}
static int process_one(char *name)
{
struct stat sb;
int rc;
rc = lstat(name, &sb);
if (rc < 0) {
if (FLAG_i_ignore_enoent && errno == ENOENT)
return 0;
bb_perror_msg("stat(%s)", name);
goto err;
}
dev_id = sb.st_dev;
if (S_ISDIR(sb.st_mode) && recurse) {
if (recursive_action(name,
ACTION_RECURSE,
apply_spec,
apply_spec,
NULL, 0) != TRUE
) {
bb_error_msg("error while labeling %s", name);
goto err;
}
} else {
rc = restore(name);
if (rc)
goto err;
}
out:
if (add_assoc) {
if (FLAG_q_quiet)
set_matchpathcon_printf(&qprintf);
matchpathcon_filespec_eval();
set_matchpathcon_printf(NULL);
matchpathcon_filespec_destroy();
}
return rc;
err:
rc = -1;
goto out;
}
int setfiles_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int setfiles_main(int argc UNUSED_PARAM, char **argv)
{
struct stat sb;
int rc, i = 0;
const char *input_filename = NULL;
char *buf = NULL;
size_t buf_len;
int flags;
llist_t *exclude_dir = NULL;
char *out_filename = NULL;
INIT_G();
if (applet_name[0] == 's') { /* "setfiles" */
/*
* setfiles:
* Recursive descent,
* Does not expand paths via realpath,
* Aborts on errors during the file tree walk,
* Try to track inode associations for conflict detection,
* Does not follow mounts,
* Validates all file contexts at init time.
*/
recurse = 1;
abort_on_error = 1;
add_assoc = 1;
/* follow_mounts = 0; - already is */
matchpathcon_flags = MATCHPATHCON_VALIDATE | MATCHPATHCON_NOTRANS;
} else {
/*
* restorecon:
* No recursive descent unless -r/-R,
* Expands paths via realpath,
* Do not abort on errors during the file tree walk,
* Do not try to track inode associations for conflict detection,
* Follows mounts,
* Does lazy validation of contexts upon use.
*/
expand_realpath = 1;
follow_mounts = 1;
matchpathcon_flags = MATCHPATHCON_NOTRANS;
/* restorecon only */
selinux_or_die();
}
set_matchpathcon_flags(matchpathcon_flags);
/* Option order must match OPT_x definitions! */
if (applet_name[0] == 'r') { /* restorecon */
flags = getopt32(argv, "^"
"de:*f:ilnpqrsvo:FWR"
"\0"
"vv:v--p:p--v:v--q:q--v",
&exclude_dir, &input_filename, &out_filename,
&verbose
);
} else { /* setfiles */
flags = getopt32(argv, "^"
"de:*f:ilnpqr:svo:FW"
IF_FEATURE_SETFILES_CHECK_OPTION("c:")
"\0"
"vv:v--p:p--v:v--q:q--v",
&exclude_dir, &input_filename, &rootpath, &out_filename,
IF_FEATURE_SETFILES_CHECK_OPTION(&policyfile,)
&verbose
);
}
argv += optind;
#if ENABLE_FEATURE_SETFILES_CHECK_OPTION
if ((applet_name[0] == 's') && (flags & OPT_c)) {
FILE *policystream;
policystream = xfopen_for_read(policyfile);
if (sepol_set_policydb_from_file(policystream) < 0) {
bb_error_msg_and_die("sepol_set_policydb_from_file on %s", policyfile);
}
fclose(policystream);
/* Only process the specified file_contexts file, not
* any .homedirs or .local files, and do not perform
* context translations. */
set_matchpathcon_flags(MATCHPATHCON_BASEONLY |
MATCHPATHCON_NOTRANS |
MATCHPATHCON_VALIDATE);
}
#endif
while (exclude_dir)
add_exclude(llist_pop(&exclude_dir));
if (flags & OPT_o) {
outfile = stdout;
if (NOT_LONE_CHAR(out_filename, '-')) {
outfile = xfopen_for_write(out_filename);
}
}
if (applet_name[0] == 'r') { /* restorecon */
if (flags & (OPT_r | OPT_R))
recurse = 1;
} else { /* setfiles */
if (flags & OPT_r)
rootpathlen = strlen(rootpath);
}
if (flags & OPT_s) {
input_filename = "-";
add_assoc = 0;
}
if (applet_name[0] == 's') { /* setfiles */
/* Use our own invalid context checking function so that
* we can support either checking against the active policy or
* checking against a binary policy file. */
set_matchpathcon_canoncon(&canoncon);
if (!argv[0])
bb_show_usage();
xstat(argv[0], &sb);
if (!S_ISREG(sb.st_mode)) {
bb_error_msg_and_die("'%s' is not a regular file", argv[0]);
}
/* Load the file contexts configuration and check it. */
rc = matchpathcon_init(argv[0]);
if (rc < 0) {
bb_simple_perror_msg_and_die(argv[0]);
}
if (nerr)
exit(EXIT_FAILURE);
argv++;
}
if (input_filename) {
ssize_t len;
FILE *f = stdin;
if (NOT_LONE_CHAR(input_filename, '-'))
f = xfopen_for_read(input_filename);
while ((len = getline(&buf, &buf_len, f)) > 0) {
buf[len - 1] = '\0';
errors |= process_one(buf);
}
if (ENABLE_FEATURE_CLEAN_UP)
fclose_if_not_stdin(f);
} else {
if (!argv[0])
bb_show_usage();
for (i = 0; argv[i]; i++) {
errors |= process_one(argv[i]);
}
}
if (FLAG_W_warn_no_match)
matchpathcon_checkmatches(argv[0]);
if (ENABLE_FEATURE_CLEAN_UP && outfile)
fclose(outfile);
return errors;
}