selinux: add LabelContext for alternate-root labeling

When operating on an alternate root (--root/--image), SELinux labels must
come from that root's policy, not the host's. This requires opening a
separate selabel_handle against the target's policy database and
remembering the root path for prefix stripping.

Introduce LabelContext to carry both, and mac_label_context_new() to
set it up: it reads the target's SELinux config, validates SELINUXTYPE to
prevent path traversal, sets the policy root, and opens a label database
scoped to the target image.

Signed-off-by: Simon de Vlieger <cmdr@supakeen.com>
This commit is contained in:
Simon de Vlieger
2026-07-31 09:49:41 +02:00
parent 6b0e3ea222
commit f2b82e5c20
4 changed files with 234 additions and 6 deletions
+16
View File
@@ -6,6 +6,7 @@
#include "btrfs-util.h"
#include "errno-util.h"
#include "label-util.h"
#include "path-util.h"
#include "selinux-util.h"
#include "smack-util.h"
@@ -97,6 +98,21 @@ int btrfs_subvol_make_label(const char *path, LabelContext *label_context) {
return mac_smack_fix(path, 0);
}
int mac_label_context_new(const char *root, LabelContext **ret) {
assert(ret);
if (empty_or_root(root)) {
*ret = NULL;
return 0;
}
return mac_selinux_label_context_new(root, ret);
}
LabelContext* mac_label_context_free(LabelContext *c) {
return mac_selinux_label_context_free(c);
}
static int init_internal(bool lazy) {
int r;
+4
View File
@@ -27,3 +27,7 @@ int btrfs_subvol_make_label(const char *path, LabelContext *label_context);
int mac_init(void);
int mac_init_lazy(void);
int mac_label_context_new(const char *root, LabelContext **ret);
LabelContext* mac_label_context_free(LabelContext *c);
DEFINE_TRIVIAL_CLEANUP_FUNC(LabelContext*, mac_label_context_free);
+209 -6
View File
@@ -19,17 +19,22 @@
#include "alloc-util.h"
#include "fd-util.h"
#include "path-util.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#endif
#include "chase.h"
#include "env-file.h"
#include "errno-util.h"
#include "label-util.h"
#include "path-util.h"
#include "selinux-util.h"
#include "smack-util.h"
#if HAVE_SELINUX
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(context_t, sym_context_free, context_freep, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(struct selabel_handle *, sym_selabel_close, selabel_closep, NULL);
typedef enum Initialized {
UNINITIALIZED,
@@ -43,6 +48,20 @@ static int last_policyload = 0;
static struct selabel_handle *label_hnd = NULL;
static bool have_status_page = false;
struct LabelContext {
struct selabel_handle *label_hnd;
int rfd;
char *root;
};
static void selinux_policy_root_reset_and_freep(char **s) {
assert(s);
if (!*s)
return;
(void) sym_selinux_set_policy_root(*s);
*s = mfree(*s);
}
static int mac_selinux_label_pre(int dir_fd, const char *path, mode_t mode, LabelContext *label_context) {
return mac_selinux_create_file_prepare_at(dir_fd, path, mode, label_context);
}
@@ -52,6 +71,11 @@ static int mac_selinux_label_post(int dir_fd, const char *path, bool created, La
return 0;
}
static const LabelOps selinux_label_ops = {
.pre = mac_selinux_label_pre,
.post = mac_selinux_label_post,
};
DLSYM_PROTOTYPE(avc_open) = NULL;
DLSYM_PROTOTYPE(context_free) = NULL;
DLSYM_PROTOTYPE(context_new) = NULL;
@@ -75,7 +99,9 @@ DLSYM_PROTOTYPE(selinux_check_access) = NULL;
DLSYM_PROTOTYPE(selinux_getenforcemode) = NULL;
DLSYM_PROTOTYPE(selinux_init_load_policy) = NULL;
DLSYM_PROTOTYPE(selinux_path) = NULL;
DLSYM_PROTOTYPE(selinux_policy_root) = NULL;
DLSYM_PROTOTYPE(selinux_set_callback) = NULL;
DLSYM_PROTOTYPE(selinux_set_policy_root) = NULL;
DLSYM_PROTOTYPE(selinux_status_close) = NULL;
DLSYM_PROTOTYPE(selinux_status_getenforce) = NULL;
DLSYM_PROTOTYPE(selinux_status_open) = NULL;
@@ -121,7 +147,9 @@ int dlopen_libselinux(int log_level) {
DLSYM_ARG(selinux_getenforcemode),
DLSYM_ARG(selinux_init_load_policy),
DLSYM_ARG(selinux_path),
DLSYM_ARG(selinux_policy_root),
DLSYM_ARG(selinux_set_callback),
DLSYM_ARG(selinux_set_policy_root),
DLSYM_ARG(selinux_status_close),
DLSYM_ARG(selinux_status_getenforce),
DLSYM_ARG(selinux_status_open),
@@ -220,10 +248,6 @@ static int open_label_db(void) {
static int selinux_init(bool force) {
#if HAVE_SELINUX
static const LabelOps label_ops = {
.pre = mac_selinux_label_pre,
.post = mac_selinux_label_post,
};
int r;
if (!mac_selinux_use())
@@ -256,7 +280,7 @@ static int selinux_init(bool force) {
return r;
}
r = label_ops_set(&label_ops);
r = label_ops_set(&selinux_label_ops);
if (r < 0)
return r;
@@ -872,3 +896,182 @@ skipped:
#endif
return RET_NERRNO(bind(fd, addr, addrlen));
}
#if HAVE_SELINUX
static int get_selinux_policy_root(int rfd, const char *root, char **ret) {
_cleanup_free_ char *policytype = NULL;
int r;
assert(rfd >= 0);
assert(root);
assert(ret);
/* Read the SELinux config from the alternate root, trying /etc and /usr/lib */
FOREACH_STRING(config_dir, "/etc/selinux/config", "/usr/lib/selinux/config") {
_cleanup_close_ int fd = -EBADF;
r = chaseat(rfd, rfd, config_dir, CHASE_MUST_BE_REGULAR, /* ret_path= */ NULL, &fd);
if (IN_SET(r, -ENOENT, -ENOTDIR, -EISDIR))
continue;
if (r < 0)
return log_debug_errno(r, "Failed to resolve SELinux config path in root '%s': %m", root);
r = parse_env_file_fd(fd, config_dir, "SELINUXTYPE", &policytype);
if (r == -ENOENT)
continue;
if (r < 0)
return log_debug_errno(r, "Failed to parse SELinux config '%s' in root '%s': %m", config_dir, root);
if (!isempty(policytype))
break;
}
if (isempty(policytype)) {
log_debug("No SELINUXTYPE configured in alternate root '%s', skipping label context setup.", root);
*ret = NULL;
return 0;
}
if (!filename_is_valid(policytype))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"SELINUXTYPE '%s' in alternate root '%s' is not a valid filename.", policytype, root);
/* Look for the policy database under the alternate root */
FOREACH_STRING(policy_dir, "/etc/selinux", "/usr/share/selinux") {
_cleanup_free_ char *p = NULL, *policy_subdir = NULL;
policy_subdir = path_join(policy_dir, policytype);
if (!policy_subdir)
return log_oom();
r = chaseat(rfd, rfd, policy_subdir, CHASE_MUST_BE_DIRECTORY, &p, /* ret_fd= */ NULL);
if (IN_SET(r, -ENOENT, -ENOTDIR))
continue;
if (r < 0) {
log_debug_errno(r, "Failed to resolve policy directory '%s' in root '%s': %m", policy_subdir, root);
continue;
}
/* Validate that the file_contexts file resolves within the root, since libselinux
* will open it with plain fopen() which follows symlinks without confinement */
_cleanup_free_ char *fc_subpath = NULL;
fc_subpath = path_join(policy_subdir, "contexts/files/file_contexts");
if (!fc_subpath)
return log_oom();
r = chaseat(rfd, rfd, fc_subpath, CHASE_MUST_BE_REGULAR, /* ret_path= */ NULL, /* ret_fd= */ NULL);
if (IN_SET(r, -ENOENT, -ENOTDIR)) {
log_debug_errno(r, "file_contexts not found at '%s' in root '%s', skipping.", fc_subpath, root);
continue;
}
if (r < 0) {
log_debug_errno(r, "Failed to resolve file_contexts '%s' in root '%s': %m", fc_subpath, root);
continue;
}
r = chaseat_prefix_root(p, root, ret);
if (r < 0)
return r;
return 1;
}
log_debug("SELinux policy directory for type '%s' not found in alternate root '%s', skipping.", policytype, root);
*ret = NULL;
return 0;
}
#endif
int mac_selinux_label_context_new(const char *root, LabelContext **ret) {
assert(root);
assert(!empty_or_root(root));
assert(ret);
#if HAVE_SELINUX
_cleanup_free_ char *policyroot = NULL, *canonical_root = NULL;
_cleanup_close_ int rfd = -EBADF;
int r;
if (mac_smack_use()) {
*ret = NULL;
return 0;
}
r = dlopen_libselinux(LOG_ERR);
if (r < 0)
return r;
mac_selinux_disable_logging();
r = chaseat(AT_FDCWD, AT_FDCWD, root, CHASE_MUST_BE_DIRECTORY, &canonical_root, &rfd);
if (r < 0)
return log_debug_errno(r, "Failed to open root directory '%s': %m", root);
r = get_selinux_policy_root(rfd, root, &policyroot);
if (r < 0)
return r;
if (r == 0) {
*ret = NULL;
return 0;
}
/* Temporarily switch the policy root so selabel_open() reads the image's file_contexts.
* The handle bakes in the database at open time, so we can reset the root immediately after.
* We must strdup the saved root because selinux_set_policy_root() frees the internal buffer. */
_cleanup_(selinux_policy_root_reset_and_freep) char *saved_policy_root = NULL;
const char *p = sym_selinux_policy_root();
if (p) {
saved_policy_root = strdup(p);
if (!saved_policy_root)
return log_oom();
}
r = RET_NERRNO(sym_selinux_set_policy_root(policyroot));
if (r < 0)
return log_debug_errno(r, "Failed to set SELinux policy root to '%s': %m", policyroot);
_cleanup_(selabel_closep) struct selabel_handle *hnd = sym_selabel_open(SELABEL_CTX_FILE, /* opts= */ NULL, /* nopts= */ 0);
int saved_errno = errno;
/* Reset immediately - the handle baked in the database at open time */
selinux_policy_root_reset_and_freep(&saved_policy_root);
if (!hnd)
return log_debug_errno(saved_errno, "Failed to open SELinux label database for alternate root '%s': %m", root);
r = label_ops_set(&selinux_label_ops);
if (r < 0 && r != -EBUSY)
return r;
_cleanup_free_ LabelContext *c = new(LabelContext, 1);
if (!c)
return log_oom();
*c = (LabelContext) {
.label_hnd = TAKE_PTR(hnd),
.rfd = TAKE_FD(rfd),
.root = TAKE_PTR(canonical_root),
};
*ret = TAKE_PTR(c);
return 0;
#else
*ret = NULL;
return 0;
#endif
}
LabelContext* mac_selinux_label_context_free(LabelContext *c) {
if (!c)
return NULL;
#if HAVE_SELINUX
if (c->label_hnd)
sym_selabel_close(c->label_hnd);
safe_close(c->rfd);
free(c->root);
#endif
return mfree(c);
}
+5
View File
@@ -41,7 +41,9 @@ extern DLSYM_PROTOTYPE(selinux_check_access);
extern DLSYM_PROTOTYPE(selinux_getenforcemode);
extern DLSYM_PROTOTYPE(selinux_init_load_policy);
extern DLSYM_PROTOTYPE(selinux_path);
extern DLSYM_PROTOTYPE(selinux_policy_root);
extern DLSYM_PROTOTYPE(selinux_set_callback);
extern DLSYM_PROTOTYPE(selinux_set_policy_root);
extern DLSYM_PROTOTYPE(selinux_status_close);
extern DLSYM_PROTOTYPE(selinux_status_getenforce);
extern DLSYM_PROTOTYPE(selinux_status_open);
@@ -114,3 +116,6 @@ int mac_selinux_create_socket_prepare(const char *label);
void mac_selinux_create_socket_clear(void);
int mac_selinux_bind(int fd, const struct sockaddr *addr, socklen_t addrlen);
int mac_selinux_label_context_new(const char *root, LabelContext **ret);
LabelContext* mac_selinux_label_context_free(LabelContext *c);