diff --git a/BUILD b/BUILD index 42974120..6727afd6 100644 --- a/BUILD +++ b/BUILD @@ -180,6 +180,8 @@ DEFAULT_STOP_TIMEOUT=XXX this, its process group is sent a SIGKILL signal which should cause it to terminate immediately. The default if unspecified is 10 seconds. (The value can be overridden for individual services via the service description). +SUPPORT_SELINUX=1|0 + Whether to build support for loading the system SELinux policy at boot. SUPPORT_CGROUPS=1|0 Whether to include support for cgroups (Linux only). SUPPORT_CAPABILITIES=1|0 diff --git a/BUILD_MESON b/BUILD_MESON index f5ee0bfa..863b8410 100644 --- a/BUILD_MESON +++ b/BUILD_MESON @@ -167,6 +167,10 @@ Custom options: build-shutdown : Whether to build the shutdown/reboot/halt utilities. Available values : enabled, disabled, auto Default value : auto + + support-selinux : Enable SELinux support. + Available values : enabled, disabled, auto + Default value : auto Running the test suite diff --git a/CONTRIBUTORS b/CONTRIBUTORS index ff3dd217..76f33e05 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -17,3 +17,4 @@ The following people (in alphabetical order) have contributed: * Oliver Amann - Code, testing, documentation * Locria Cyber - Code, documentation * q66 - Code, testing, documentation. + * Rahul Sandhu - Code diff --git a/build/Makefile b/build/Makefile index d44f7a5f..34ddf32c 100644 --- a/build/Makefile +++ b/build/Makefile @@ -19,7 +19,8 @@ includes/mconfig.h: ../mconfig tools/mconfig-gen.cc version.conf $(if $(SUPPORT_IOPRIO),SUPPORT_IOPRIO=$(SUPPORT_IOPRIO),) \ $(if $(SUPPORT_OOM_ADJ),SUPPORT_OOM_ADJ=$(SUPPORT_OOM_ADJ),) \ $(if $(USE_UTMPX),USE_UTMPX=$(USE_UTMPX),) \ - $(if $(USE_INITGROUPS),USE_INITGROUPS=$(USE_INITGROUPS),) > includes/mconfig.h + $(if $(USE_INITGROUPS),USE_INITGROUPS=$(USE_INITGROUPS),) \ + $(if $(SUPPORT_SELINUX),SUPPORT_SELINUX=$(SUPPORT_SELINUX),) > includes/mconfig.h clean: rm -f includes/mconfig.h diff --git a/build/mconfig.mesontemplate b/build/mconfig.mesontemplate index 268363ab..3e1cf532 100644 --- a/build/mconfig.mesontemplate +++ b/build/mconfig.mesontemplate @@ -8,6 +8,7 @@ #mesondefine USE_UTMPX #mesondefine USE_INITGROUPS #mesondefine SUPPORT_CGROUPS +#mesondefine SUPPORT_SELINUX #mesondefine SUPPORT_CAPABILITIES #mesondefine SUPPORT_IOPRIO #mesondefine SUPPORT_OOM_ADJ diff --git a/build/tools/mconfig-gen.cc b/build/tools/mconfig-gen.cc index 5397f418..2d70eb73 100644 --- a/build/tools/mconfig-gen.cc +++ b/build/tools/mconfig-gen.cc @@ -89,6 +89,9 @@ int main(int argc, char **argv) if (vars.find("DEFAULT_AUTO_RESTART") != vars.end()) { cout << "#define DEFAULT_AUTO_RESTART " << vars["DEFAULT_AUTO_RESTART"] << "\n"; } + if (vars.find("SUPPORT_SELINUX") != vars.end()) { + cout << "#define SUPPORT_SELINUX " << vars["SUPPORT_SELINUX"] << "\n"; + } cout << "\n// Constants\n"; cout << "\nconstexpr static char DINIT_VERSION[] = " << stringify(vars["VERSION"]) << ";\n"; diff --git a/configure b/configure index 21264d1e..808e18f3 100755 --- a/configure +++ b/configure @@ -213,6 +213,8 @@ Optional options: --enable-initgroups Enable initialization of supplementary groups for run-as [Enabled] --disable-initgroups Disable initialization of supplementary groups for run-as + --enable-selinux Enable SELinux support [Disabled] + --disable-selinux Disable SELinux support --enable-auto-restart Enable auto-restart for services by default (Deprecated; use --default-auto-restart=...) --disable-auto-restart Disable auto-restart for services by default (Deprecated; @@ -283,6 +285,7 @@ for var in PREFIX \ SUPPORT_OOM_ADJ \ USE_UTMPX \ USE_INITGROUPS \ + SUPPORT_SELINUX \ SYSCONTROLSOCKET \ STRIPOPTS do @@ -324,6 +327,8 @@ for arg in "$@"; do --disable-initgroups|--enable-initgroups=no) USE_INITGROUPS=0 ;; --enable-auto-restart|--enable-auto-restart=yes) DEFAULT_AUTO_RESTART=ALWAYS ;; # Deprecated --disable-auto-restart|--enable-auto-restart=no) DEFAULT_AUTO_RESTART=NEVER ;; # Deprecated + --enable-selinux|--enable-selinux=yes) SUPPORT_SELINUX=1 ;; + --disable-selinux|--enable-selinux=no) SUPPORT_SELINUX=0 ;; --enable-strip|--enable-strip=yes) STRIPOPTS="-s" ;; --disable-strip|--enable-strip=no) STRIPOPTS="" ;; --default-auto-restart=never) DEFAULT_AUTO_RESTART=NEVER ;; @@ -355,6 +360,7 @@ done : "${DEFAULT_START_TIMEOUT:="60"}" : "${DEFAULT_STOP_TIMEOUT:="10"}" : "${USE_INITGROUPS:="1"}" +: "${SUPPORT_SELINUX:="0"}" if [ "$PLATFORM" = "Linux" ]; then : "${BUILD_SHUTDOWN:="yes"}" : "${SUPPORT_CGROUPS:="1"}" @@ -477,6 +483,9 @@ fi if [ "$AUTO_LDFLAGS_BASE" = true ] && [ "$PLATFORM" = FreeBSD ]; then try_ld_argument LDFLAGS_BASE -lrt fi +if [ "$AUTO_LDFLAGS_BASE" = true ] && [ "$SUPPORT_SELINUX" = "1" ]; then + try_ld_argument LDFLAGS_BASE -lselinux +fi if [ "$SUPPORT_CAPABILITIES" != 0 ]; then if [ "$AUTO_LDFLAGS_LIBCAP" = true ]; then try_ld_argument LDFLAGS_LIBCAP -lcap @@ -587,6 +596,7 @@ LDFLAGS_LIBCAP=$LDFLAGS_LIBCAP # Feature settings SUPPORT_CGROUPS=$SUPPORT_CGROUPS USE_INITGROUPS=$USE_INITGROUPS +SUPPORT_SELINUX=$SUPPORT_SELINUX SUPPORT_CAPABILITIES=$SUPPORT_CAPABILITIES SUPPORT_IOPRIO=$SUPPORT_IOPRIO SUPPORT_OOM_ADJ=$SUPPORT_OOM_ADJ diff --git a/doc/linux/SELINUX.md b/doc/linux/SELINUX.md new file mode 100644 index 00000000..a9819824 --- /dev/null +++ b/doc/linux/SELINUX.md @@ -0,0 +1,46 @@ +# Dinit SELinux Awareness + +Dinit has support for basic SELinux awareness. This document is intended to +outline the extent and inner workings of Dinit's SELinux awareness. The reader +is assumed to be knowledgeable about the basics of [SELinux](https://github.com/SELinuxProject/selinux-notebook) and Dinit. + +Dinit needs to be built with SELinux support (see [BUILD](/BUILD)) to enable the features that are +mentioned in this document. + +## Loading the system SELinux policy +When booted as the system init system, dinit by default will attempt to load the +system's SELinux policy and transition itself to a context specified by that policy +if not already done so in earlier boot (e.g. by an initramfs). This behaviour may be +disabled by passing dinit the `--disable-selinux-policy` flag. As dinit will always +be PID1 in this senario, this can be done by appending the flag to the kernel cmdline. + +If not already mounted in earlier boot (e.g. by an initramfs), dinit will mount `/sys`, +and selinuxfs (typically `/sys/fs/selinux`) during the call to `selinux_init_load_policy(3)`. + +The following flowchart provides an overview of the process of loading the policy: +```mermaid +flowchart TD + A[Start] --> B{"Is dinit running as the init system (PID1)?"} + B -->|Yes| C{Have we been requested to not load the SELinux policy?} + B -->|No| D[Continue rest of dinit initialization] + C -->|Yes| D + C -->|No| E[Is the SELinux policy already loaded?] + E -->|Yes| D + E --> |No| F{Is /proc mounted?} + F --> |Yes| J + F --> |No| G[Attempt to mount /proc] + G --> H{Could we successfully mount /proc?} + H --> |Yes| J + H -->|No| I[Error exit early] + J[Attempt to load the SELinux policy] + J --> K{Did the SELinux policy load succeed?} + K -->|Yes| L[Attempt to calculate our new context and transition] + K -->|No| I + L --> M{Did we successfully transition?} + M -->|Yes| O{Did we mount /proc?} + M -->|No| N[Log an error to stderr] + N --> O + O -->|Yes| P[Unmount /proc] + O -->|No| D + P --> D +``` diff --git a/doc/manpages/dinit.8.m4 b/doc/manpages/dinit.8.m4 index 89abe1aa..3b04218e 100644 --- a/doc/manpages/dinit.8.m4 +++ b/doc/manpages/dinit.8.m4 @@ -113,6 +113,10 @@ If service description settings contain relative cgroup paths, they will be reso this path. This option is only available if \fBdinit\fR is built with cgroups support. .TP +\fB\-\-disable\-selinux\-policy\fR +Disable loading of the system SELinux policy. +This option is only available if \fBdinit\fR is built with SELinux support. +.TP \fB\-\-help\fR Display brief help text and then exit. .TP @@ -298,6 +302,16 @@ There are several ways to work around this. Service names following the \fB\-\-container\fR (\fB\-o\fR) or \fB\-\-system\-mgr\fR (\fB\-m\fR) options are not ignored. Also, the \fB\-\-service\fR (\fB\-t\fR) option can be used to force a service name to be recognised regardless of operating mode. .\" +.SH SELINUX SUPPORT +.LP +When running as PID 1 on a SELinux enabled machine, \fBdinit\fR will by default load the system's SELinux policy. +This behaviour can be disabled by passing \fB\-\-disable\-selinux\-policy\fR to dinit through the kernel cmdline. +.LP +When loading the SELinux policy, dinit will automatically mount a few special filesystems needed to successfully load the policy. +\fBsysfs\fR will be mounted at \fB/sys\fR, and \fBselinuxfs\fR will be mounted at \fB/sys/fs/selinux\fR. +\fBdinit\fR will not unmount either. +\fBprocfs\fR will also be mounted at \fB/proc\fR, but \fBdinit\fR will unmount it when done with it. +.\" .SH FILES .\" .TP diff --git a/meson.build b/meson.build index 21e7c58e..b857a8eb 100644 --- a/meson.build +++ b/meson.build @@ -36,6 +36,7 @@ support_ioprio = get_option('support-ioprio') support_oom_adj = get_option('support-oom-adj') use_utmpx = get_option('use-utmpx') use_initgroups = get_option('use-initgroups') +support_selinux = get_option('support-selinux') default_auto_restart = get_option('default-auto-restart') default_start_timeout = get_option('default-start-timeout').to_string() default_stop_timeout = get_option('default-stop-timeout').to_string() @@ -61,6 +62,7 @@ endif ## Dependencies libcap_dep = dependency('libcap', required: support_capabilities) +libselinux_dep = dependency('libselinux', version : '>= 2.1.9', required : support_selinux) ## Prepare mconfig.h mconfig_data.set_quoted('DINIT_VERSION', version) @@ -71,6 +73,7 @@ mconfig_data.set('DEFAULT_AUTO_RESTART', default_auto_restart) mconfig_data.set('DEFAULT_START_TIMEOUT', default_start_timeout) mconfig_data.set('DEFAULT_STOP_TIMEOUT', default_stop_timeout) mconfig_data.set10('USE_INITGROUPS', use_initgroups) +mconfig_data.set10('SUPPORT_SELINUX', libselinux_dep.found() or support_selinux.enabled()) mconfig_data.set10('SUPPORT_CGROUPS', support_cgroups.auto() and platform == 'linux' or support_cgroups.enabled()) mconfig_data.set10('SUPPORT_CAPABILITIES', libcap_dep.found() and not support_capabilities.disabled()) mconfig_data.set10('SUPPORT_IOPRIO', support_ioprio.auto() and platform == 'linux' or support_ioprio.enabled()) diff --git a/meson_options.txt b/meson_options.txt index af92b7eb..909b4daf 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -109,3 +109,9 @@ option( value : 'auto', description : 'Building shutdown/reboot/soft-reboot/halt or not.' ) +option( + 'support-selinux', + type : 'feature', + value : 'auto', + description : 'SELinux support' +) diff --git a/src/dinit.cc b/src/dinit.cc index 31eaa60c..5e034361 100644 --- a/src/dinit.cc +++ b/src/dinit.cc @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,12 @@ #include "mconfig.h" +#if SUPPORT_SELINUX +#include +#include +#include +#endif + /* * When running as the system init process, Dinit processes the following signals: * @@ -211,6 +218,10 @@ struct options { // list of services to start std::list services_to_start; + +#ifdef SUPPORT_SELINUX + bool load_selinux_policy = true; +#endif }; // Process a command line argument (and possibly its follow-up value) @@ -369,6 +380,11 @@ static int process_commandline_arg(char **argv, int argc, int &i, options &opts) } } #endif + #ifdef SUPPORT_SELINUX + else if (strcmp(argv[i], "--disable-selinux-policy") == 0) { + opts.load_selinux_policy = false; + } + #endif else if (strcmp(argv[i], "--service") == 0 || strcmp(argv[i], "-t") == 0) { if (++i < argc && argv[i][0] != '\0') { services_to_start.push_back(argv[i]); @@ -404,6 +420,9 @@ static int process_commandline_arg(char **argv, int argc, int &i, options &opts) " --cgroup-path , -b \n" " cgroup base path (for resolving relative paths)\n" #endif + #ifdef SUPPORT_SELINUX + " --disable-selinux-policy don't load the system SELinux policy\n" + #endif " --log-file , -l log to the specified file\n" " --quiet, -q disable output to standard output\n" " , --service , -t \n" @@ -458,6 +477,106 @@ static int process_commandline_arg(char **argv, int argc, int &i, options &opts) return 0; } +#if SUPPORT_SELINUX +// Load the system SELinux policy and transition ourselves to it. +// Parameters: +// exe - the path that we are invoked with (to calculate our new security context to transition +// into.) +// Returns: +// If we fail to load the system SELinux policy when requested to load in enforcing mode, return +// false, otherwise, return true. +// This function will attempt to mount /sys and /proc if they aren't already mounted. /sys will +// remain mounted after returning, and it is possible for /sys to still remain mounted despite +// returning false. This function will attempt to unmount /proc if it was responsible for mounting +// it, but lazily unmounts it using MNT_DETACH so while /proc will be unavailable for new accesses, +// it is not guarenteed to be unmounted. +// When successful, this will cause SELinux labels as per the policy to be attached to processes +// (and file descriptors owned by those processes). The SELinux framework will begin to enforce +// restrictions on access based on these labels and the loaded policy. +// We might lose access to any file descriptors we have open when this is called (since they will +// still be labelled with the current policy's representation of the kernel placeholder context), +// so it is best done early (i.e. before we start opening file descriptors). +// Returning true does not guarantee that the system SELinux policy has successfully been loaded. +// The return value is only an indication of whether or not dinit should bail. +static bool selinux_transition(const char *exe) +{ + if (is_selinux_enabled() == 1) return true; + + // If we fail to mount /proc, getcon_raw(3), getfilecon_raw(3), and setcon_raw(3) can be + // expected to fail later on. However, the burden of checking is not on us; those functions + // may also fail due to our own inability to access /proc later on due to e.g. security policy. + // Let's just store the return code so we can umount later when we're done with /proc if + // succesful. + auto proc_mount_rc = mount("proc", "/proc", "proc", 0, 0); + + char *current_context = nullptr; + char *file_context = nullptr; + security_class_t security_class; + char *new_context = nullptr; + + int enforce = 0; + // We don't need to worry about the enforcing=0 kernel cmdline option or parsing + // /etc/selinux/config, selinux_init_load_policy(3) will handle all cases for us. + if (selinux_init_load_policy(&enforce) != 0) { + if (enforce > 0) { + // As we bail here, we can't use the log, so use cerr instead. + std::cerr << "Failed to load SELinux policy when requested to load in enforcing mode." + << std::endl; + return false; + } + log(loglevel_t::ERROR, "Failed to load SELinux policy while set to permissive, ignoring."); + // We can't transition ourselves if we failed to load the policy, so return early. + return true; + } + + // The newly loaded SELinux policy may stop us from calculating our new label, by preventing us + // (in our current domain, the inital SID's representation in the loaded policy) from accessing + // certain resources that are needed to calculate our label, for example, but not limited to, + // the xattrs for `exe`. This is a policy choice, and not a dinit runtime issue. Let's continue + // the boot process regardless, but still log a warning where applicable. + + // As indicated by the getcon(3) manpage, getcon_raw(3) may return 0 indicating success and set + // current_context to NULL if SELinux is disabled, or other LSMs are at play. It's best to + // check the pointer we get back in addition to the return value. + if (getcon_raw(¤t_context) < 0 || current_context == nullptr) { + log(loglevel_t::ERROR, "Failed to get current SELinux context: ", strerror(errno)); + goto cleanup; + } + + if (getfilecon_raw(exe, &file_context) < 0) { + log(loglevel_t::ERROR, "Failed to get SELinux file context for ", exe, ": ", strerror(errno)); + goto cleanup; + } + + security_class = string_to_security_class("process"); + if (security_class == 0) { + log(loglevel_t::ERROR, "Failed to get SELinux security class for process"); + goto cleanup; + } + + if (security_compute_create_raw(current_context, file_context, security_class, &new_context) < 0) { + log(loglevel_t::ERROR, "Failed to compute SELinux create context: ", strerror(errno)); + goto cleanup; + } + + // The loaded SELinux policy may prevent the domain transition from our current domain to the + // domain specified for us in the policy. This is a policy choice, and not a dinit runtime + // issue. Let's continue the boot process regardless, but still log a warning. + if (setcon_raw(new_context) < 0) { + log(loglevel_t::ERROR, "Failed to set SELinux transition context to ", + new_context, ": ", strerror(errno)); + goto cleanup; + } + +cleanup: + if (current_context) freecon(current_context); + if (file_context) freecon(file_context); + if (new_context) freecon(new_context); + if (proc_mount_rc == 0) umount2("/proc", MNT_DETACH); + return true; +} +#endif + // Main entry point int dinit_main(int argc, char **argv) { @@ -491,6 +610,18 @@ int dinit_main(int argc, char **argv) } } +#if SUPPORT_SELINUX + // Error exit if we are PID 1 and fail to load the selinux policy. + // + // This should be done directly after argument parsing, it's best to do this as early as + // possible to get init in the domain specified in the policy, and hence confine it, quickly. + // + // If selinux_transition fails, the system is not in the state requested by the user, and there + // is nothing we can do about it. Instead of continuing to boot the rest of the system without + // loading the user's policy, let's bail now to avoid an insecure and untrusted state. + if (am_system_mgr && am_system_init && opts.load_selinux_policy && !selinux_transition(argv[0])) return 1; +#endif + if (am_system_mgr) { // setup STDIN, STDOUT, STDERR so that we can use them int onefd = open("/dev/console", O_RDONLY, 0); @@ -1214,6 +1345,9 @@ static void printVersion() #if USE_INITGROUPS +1 #endif +#if SUPPORT_SELINUX + +1 +#endif #if SUPPORT_CAPABILITIES +1 #endif @@ -1235,6 +1369,9 @@ static void printVersion() #if USE_INITGROUPS " supplemental-groups" #endif +#if SUPPORT_SELINUX + " selinux" +#endif #if SUPPORT_CAPABILITIES " capabilities" #endif diff --git a/src/meson.build b/src/meson.build index bb1f976b..c2f4ab68 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,7 +28,7 @@ misc_args = { 'include_directories': default_incdir, 'install': true, 'install_dir': sbindir, - 'dependencies': [libcap_dep] + 'dependencies': [libcap_dep, libselinux_dep] } ## src/'s defines for igr-tests/