From d89294e25a5eba4f443a3f2b037a3cf9bbc54296 Mon Sep 17 00:00:00 2001 From: Davin McCall Date: Sat, 10 Aug 2019 14:20:40 +1000 Subject: [PATCH] Beginnings of "dinitcheck" utility This will be a utility to check service configuration for errors/lint. --- src/Makefile | 7 +- src/dinitcheck.cc | 533 ++++++++++++++++++++++++++++++++++++ src/includes/dinit-util.h | 11 + src/includes/load-service.h | 356 +++++++++++++++++++++++- src/includes/proc-service.h | 10 - src/includes/service.h | 52 +--- src/load-service.cc | 271 ------------------ 7 files changed, 897 insertions(+), 343 deletions(-) create mode 100644 src/dinitcheck.cc diff --git a/src/Makefile b/src/Makefile index 9b0b3ef..b6dce35 100644 --- a/src/Makefile +++ b/src/Makefile @@ -12,9 +12,9 @@ endif dinit_objects = dinit.o load-service.o service.o proc-service.o baseproc-service.o control.o dinit-log.o \ dinit-main.o run-child-proc.o -objects = $(dinit_objects) dinitctl.o shutdown.o +objects = $(dinit_objects) dinitctl.o dinitcheck.o shutdown.o -all: dinit dinitctl $(SHUTDOWN) +all: dinit dinitctl dinitcheck $(SHUTDOWN) # Look for a suitable build config file and use it. ../mconfig: @@ -41,6 +41,9 @@ dinit: $(dinit_objects) dinitctl: dinitctl.o $(CXX) -o dinitctl dinitctl.o $(LDFLAGS) +dinitcheck: dinitcheck.o + $(CXX) -o dinitcheck dinitcheck.o $(LDFLAGS) + shutdown: shutdown.o $(CXX) -o shutdown shutdown.o $(LDFLAGS) diff --git a/src/dinitcheck.cc b/src/dinitcheck.cc new file mode 100644 index 0000000..2865e17 --- /dev/null +++ b/src/dinitcheck.cc @@ -0,0 +1,533 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "dinit-util.h" +#include "service-constants.h" +#include "load-service.h" + +// dinitcheck: utility to check Dinit configuration for correctness/lint + +using string = std::string; +using string_iterator = std::string::iterator; + +static const char *user_home_path = nullptr; + +// Get user home (and set user_home_path). (The return may become invalid after +// changing the evironment (HOME variable) or using the getpwuid() function). +static const char * get_user_home() +{ + if (user_home_path == nullptr) { + user_home_path = getenv("HOME"); + if (user_home_path == nullptr) { + struct passwd * pwuid_p = getpwuid(getuid()); + if (pwuid_p != nullptr) { + user_home_path = pwuid_p->pw_dir; + } + } + } + return user_home_path; +} + +class prelim_dep +{ + std::string name; + dependency_type dep_type; + + public: + prelim_dep(std::string &name_p, dependency_type dep_type_p) + : name(name_p), dep_type(dep_type_p) { } + prelim_dep(std::string &&name_p, dependency_type dep_type_p) + : name(std::move(name_p)), dep_type(dep_type_p) { } +}; + +class service_record +{ +public: + service_record(std::string name, std::list dependencies_p) : dependencies(dependencies_p) {} + + std::string name; + bool finished_loading = false; // flag used to detect cyclic dependencies + std::list dependencies; +}; + +using service_set_t = std::map; + +service_record *load_service(service_set_t &services, const std::string &name, + const std::vector &service_dirs); + +int main(int argc, char **argv) +{ + using namespace std; + + bool add_all_service_dirs = false; + bool for_system = false; + const char * service_dir = nullptr; + bool service_dir_dynamic = false; // service_dir dynamically allocated? + + std::vector services_to_check; + + // Figure out service dirs + /* service directory name */ + if (service_dir == nullptr && ! for_system) { + const char * userhome = get_user_home(); + if (userhome != nullptr) { + const char * user_home = get_user_home(); + size_t user_home_len = strlen(user_home); + size_t dinit_d_len = strlen("/dinit.d"); + size_t full_len = user_home_len + dinit_d_len + 1; + char *service_dir_w = new char[full_len]; + std::memcpy(service_dir_w, user_home, user_home_len); + std::memcpy(service_dir_w + user_home_len, "/dinit.d", dinit_d_len); + service_dir_w[full_len - 1] = 0; + + service_dir = service_dir_w; + service_dir_dynamic = true; + } + } + + if (service_dir == nullptr) { + service_dir = "/etc/dinit.d"; + add_all_service_dirs = true; + } + + std::vector service_dirs; + + service_dirs.emplace_back(service_dir, service_dir_dynamic); + if (add_all_service_dirs) { + service_dirs.emplace_back("/usr/local/lib/dinit.d", false); + service_dirs.emplace_back("/lib/dinit.d", false); + } + + // Temporary, for testing: + services_to_check.push_back("boot"); + + // Load named service(s) + std::map service_set; + + // - load the service, store dependencies as strings + // - recurse + + // additional: check chain-to, other lint + + for (const auto &name : services_to_check) { + try { + load_service(service_set, name, service_dirs); + } + catch (service_load_exc &exc) { + std::cerr << "Unable to load service '" << name << "': " << exc.exc_description << "\n"; + } + } + + return 0; +} + +static void report_error(dinit_load::setting_exception &exc, const std::string &service_name, const char *setting_name) +{ + std::cerr << "Service '" << service_name << "', " << setting_name << ": " << exc.get_info() << "\n"; +} + +static void report_error(std::system_error &exc, const std::string &service_name) +{ + std::cerr << "Service '" << service_name << "', error reading service description: " << exc.what() << "\n"; +} + +static void report_dir_error(const char *service_name, const std::string &dirpath) +{ + std::cerr << "Service '" << service_name << "', error reading dependencies from directory " << dirpath + << ": " << strerror(errno) << "\n"; +} + +// Process a dependency directory - filenames contained within correspond to service names which +// are loaded and added as a dependency of the given type. Expected use is with a directory +// containing symbolic links to other service descriptions, but this isn't required. +// Failure to read the directory contents, or to find a service listed within, is not considered +// a fatal error. +static void process_dep_dir(const char *servicename, + const string &service_filename, + std::list &deplist, const std::string &depdirpath, + dependency_type dep_type) +{ + std::string depdir_fname = combine_paths(parent_path(service_filename), depdirpath.c_str()); + + DIR *depdir = opendir(depdir_fname.c_str()); + if (depdir == nullptr) { + report_dir_error(servicename, depdirpath); + return; + } + + errno = 0; + dirent * dent = readdir(depdir); + while (dent != nullptr) { + char * name = dent->d_name; + if (name[0] != '.') { + deplist.emplace_back(name, dep_type); + } + dent = readdir(depdir); + } + + if (errno != 0) { + report_dir_error(servicename, depdirpath); + } + + closedir(depdir); +} + +service_record *load_service(service_set_t &services, const std::string &name, + const std::vector &service_dirs) +{ + using namespace std; + using namespace dinit_load; + + auto found = services.find(name); + if (found != services.end()) { + return found->second; + } + + string service_filename; + ifstream service_file; + + // Couldn't find one. Have to load it. + for (auto &service_dir : service_dirs) { + service_filename = service_dir.get_dir(); + if (*(service_filename.rbegin()) != '/') { + service_filename += '/'; + } + service_filename += name; + + service_file.open(service_filename.c_str(), ios::in); + if (service_file) break; + } + + if (! service_file) { + throw service_not_found(string(name)); + } + + string command; + list> command_offsets; + string stop_command; + list> stop_command_offsets; + string working_dir; + string pid_file; + string env_file; + + bool do_sub_vars = false; + + service_type_t service_type = service_type_t::PROCESS; + std::list depends; + string logfile; + service_flags_t onstart_flags; + int term_signal = -1; // additional termination signal + bool auto_restart = false; + bool smooth_recovery = false; + string socket_path; + int socket_perms = 0666; + // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an + // invalid value, so it's safe to assume that we can do the same: + uid_t socket_uid = -1; + gid_t socket_gid = -1; + // Restart limit interval / count; default is 10 seconds, 3 restarts: + timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 }; + int max_restarts = 3; + timespec restart_delay = { .tv_sec = 0, .tv_nsec = 200000000 }; + timespec stop_timeout = { .tv_sec = 10, .tv_nsec = 0 }; + timespec start_timeout = { .tv_sec = 60, .tv_nsec = 0 }; + std::vector rlimits; + + int readiness_fd = -1; // readiness fd in service process + std::string readiness_var; // environment var to hold readiness fd + + uid_t run_as_uid = -1; + gid_t run_as_gid = -1; + + string chain_to_name; + + #if USE_UTMPX + char inittab_id[sizeof(utmpx().ut_id)] = {0}; + char inittab_line[sizeof(utmpx().ut_line)] = {0}; + #endif + + string line; + service_file.exceptions(ios::badbit); + + try { + process_service_file(name, service_file, + [&](string &line, string &setting, string_iterator &i, string_iterator &end) -> void { + try { + if (setting == "command") { + command = read_setting_value(i, end, &command_offsets); + } + else if (setting == "working-dir") { + working_dir = read_setting_value(i, end, nullptr); + } + else if (setting == "env-file") { + env_file = read_setting_value(i, end, nullptr); + } + else if (setting == "socket-listen") { + socket_path = read_setting_value(i, end, nullptr); + } + else if (setting == "socket-permissions") { + string sock_perm_str = read_setting_value(i, end, nullptr); + std::size_t ind = 0; + try { + socket_perms = std::stoi(sock_perm_str, &ind, 8); + if (ind != sock_perm_str.length()) { + throw std::logic_error(""); + } + } + catch (std::logic_error &exc) { + throw service_description_exc(name, "socket-permissions: Badly-formed or " + "out-of-range numeric value"); + } + } + else if (setting == "socket-uid") { + string sock_uid_s = read_setting_value(i, end, nullptr); + socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid); + } + else if (setting == "socket-gid") { + string sock_gid_s = read_setting_value(i, end, nullptr); + socket_gid = parse_gid_param(sock_gid_s, name); + } + else if (setting == "stop-command") { + stop_command = read_setting_value(i, end, &stop_command_offsets); + } + else if (setting == "pid-file") { + pid_file = read_setting_value(i, end); + } + else if (setting == "depends-on") { + string dependency_name = read_setting_value(i, end); + depends.emplace_back(std::move(dependency_name), dependency_type::REGULAR); + } + else if (setting == "depends-ms") { + string dependency_name = read_setting_value(i, end); + depends.emplace_back(dependency_name, dependency_type::MILESTONE); + } + else if (setting == "waits-for") { + string dependency_name = read_setting_value(i, end); + depends.emplace_back(dependency_name, dependency_type::WAITS_FOR); + } + else if (setting == "waits-for.d") { + string waitsford = read_setting_value(i, end); + process_dep_dir(name.c_str(), service_filename, depends, waitsford, + dependency_type::WAITS_FOR); + } + else if (setting == "logfile") { + logfile = read_setting_value(i, end); + } + else if (setting == "restart") { + string restart = read_setting_value(i, end); + auto_restart = (restart == "yes" || restart == "true"); + } + else if (setting == "smooth-recovery") { + string recovery = read_setting_value(i, end); + smooth_recovery = (recovery == "yes" || recovery == "true"); + } + else if (setting == "type") { + string type_str = read_setting_value(i, end); + if (type_str == "scripted") { + service_type = service_type_t::SCRIPTED; + } + else if (type_str == "process") { + service_type = service_type_t::PROCESS; + } + else if (type_str == "bgprocess") { + service_type = service_type_t::BGPROCESS; + } + else if (type_str == "internal") { + service_type = service_type_t::INTERNAL; + } + else { + throw service_description_exc(name, "Service type must be one of: \"scripted\"," + " \"process\", \"bgprocess\" or \"internal\""); + } + } + else if (setting == "options") { + std::list> indices; + string onstart_cmds = read_setting_value(i, end, &indices); + for (auto indexpair : indices) { + string option_txt = onstart_cmds.substr(indexpair.first, + indexpair.second - indexpair.first); + if (option_txt == "starts-rwfs") { + onstart_flags.rw_ready = true; + } + else if (option_txt == "starts-log") { + onstart_flags.log_ready = true; + } + else if (option_txt == "no-sigterm") { + onstart_flags.no_sigterm = true; + } + else if (option_txt == "runs-on-console") { + onstart_flags.runs_on_console = true; + // A service that runs on the console necessarily starts on console: + onstart_flags.starts_on_console = true; + onstart_flags.shares_console = false; + } + else if (option_txt == "starts-on-console") { + onstart_flags.starts_on_console = true; + onstart_flags.shares_console = false; + } + else if (option_txt == "shares-console") { + onstart_flags.shares_console = true; + onstart_flags.runs_on_console = false; + onstart_flags.starts_on_console = false; + } + else if (option_txt == "pass-cs-fd") { + onstart_flags.pass_cs_fd = true; + } + else if (option_txt == "start-interruptible") { + onstart_flags.start_interruptible = true; + } + else if (option_txt == "skippable") { + onstart_flags.skippable = true; + } + else if (option_txt == "signal-process-only") { + onstart_flags.signal_process_only = true; + } + else { + throw service_description_exc(name, "Unknown option: " + option_txt); + } + } + } + else if (setting == "load-options") { + std::list> indices; + string load_opts = read_setting_value(i, end, &indices); + for (auto indexpair : indices) { + string option_txt = load_opts.substr(indexpair.first, + indexpair.second - indexpair.first); + if (option_txt == "sub-vars") { + // substitute environment variables in command line + do_sub_vars = true; + } + else if (option_txt == "no-sub-vars") { + do_sub_vars = false; + } + else { + throw service_description_exc(name, "Unknown load option: " + option_txt); + } + } + } + else if (setting == "term-signal" || setting == "termsignal") { + // Note: "termsignal" supported for legacy reasons. + string signame = read_setting_value(i, end, nullptr); + int signo = signal_name_to_number(signame); + if (signo == -1) { + throw service_description_exc(name, "Unknown/unsupported termination signal: " + + signame); + } + else { + term_signal = signo; + } + } + else if (setting == "restart-limit-interval") { + string interval_str = read_setting_value(i, end, nullptr); + parse_timespec(interval_str, name, "restart-limit-interval", restart_interval); + } + else if (setting == "restart-delay") { + string rsdelay_str = read_setting_value(i, end, nullptr); + parse_timespec(rsdelay_str, name, "restart-delay", restart_delay); + } + else if (setting == "restart-limit-count") { + string limit_str = read_setting_value(i, end, nullptr); + max_restarts = parse_unum_param(limit_str, name, std::numeric_limits::max()); + } + else if (setting == "stop-timeout") { + string stoptimeout_str = read_setting_value(i, end, nullptr); + parse_timespec(stoptimeout_str, name, "stop-timeout", stop_timeout); + } + else if (setting == "start-timeout") { + string starttimeout_str = read_setting_value(i, end, nullptr); + parse_timespec(starttimeout_str, name, "start-timeout", start_timeout); + } + else if (setting == "run-as") { + string run_as_str = read_setting_value(i, end, nullptr); + run_as_uid = parse_uid_param(run_as_str, name, &run_as_gid); + } + else if (setting == "chain-to") { + chain_to_name = read_setting_value(i, end, nullptr); + } + else if (setting == "ready-notification") { + string notify_setting = read_setting_value(i, end, nullptr); + if (starts_with(notify_setting, "pipefd:")) { + readiness_fd = parse_unum_param(notify_setting.substr(7 /* len 'pipefd:' */), + name, std::numeric_limits::max()); + } + else if (starts_with(notify_setting, "pipevar:")) { + readiness_var = notify_setting.substr(8 /* len 'pipevar:' */); + if (readiness_var.empty()) { + throw service_description_exc(name, "Invalid pipevar variable name " + "in ready-notification"); + } + } + else { + throw service_description_exc(name, "Unknown ready-notification setting: " + + notify_setting); + } + } + else if (setting == "inittab-id") { + string inittab_setting = read_setting_value(i, end, nullptr); + #if USE_UTMPX + if (inittab_setting.length() > sizeof(inittab_id)) { + throw service_description_exc(name, "inittab-id setting is too long"); + } + strncpy(inittab_id, inittab_setting.c_str(), sizeof(inittab_id)); + #endif + } + else if (setting == "inittab-line") { + string inittab_setting = read_setting_value(i, end, nullptr); + #if USE_UTMPX + if (inittab_setting.length() > sizeof(inittab_line)) { + throw service_description_exc(name, "inittab-line setting is too long"); + } + strncpy(inittab_line, inittab_setting.c_str(), sizeof(inittab_line)); + #endif + } + else if (setting == "rlimit-nofile") { + string nofile_setting = read_setting_value(i, end, nullptr); + service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_NOFILE); + parse_rlimit(line, name, "rlimit-nofile", nofile_limits); + } + else if (setting == "rlimit-core") { + string nofile_setting = read_setting_value(i, end, nullptr); + service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_CORE); + parse_rlimit(line, name, "rlimit-core", nofile_limits); + } + else if (setting == "rlimit-data") { + string nofile_setting = read_setting_value(i, end, nullptr); + service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_DATA); + parse_rlimit(line, name, "rlimit-data", nofile_limits); + } + else if (setting == "rlimit-addrspace") { + #if defined(RLIMIT_AS) + string nofile_setting = read_setting_value(i, end, nullptr); + service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_AS); + parse_rlimit(line, name, "rlimit-addrspace", nofile_limits); + #endif + } + else { + throw service_description_exc(name, "Unknown setting: " + setting); + } + } + catch (setting_exception &exc) { + report_error(exc, name, setting.c_str()); + } + }); + } + catch (std::system_error &sys_err) + { + report_error(sys_err, name); + return nullptr; + } + + return new service_record(name, depends); +} diff --git a/src/includes/dinit-util.h b/src/includes/dinit-util.h index 7185370..bd28c2e 100644 --- a/src/includes/dinit-util.h +++ b/src/includes/dinit-util.h @@ -75,4 +75,15 @@ inline const char * base_name(const char *path) return basen; } +// Check if one string starts with another +inline bool starts_with(std::string s, const char *prefix) +{ + const char * sp = s.c_str(); + while (*sp != 0 && *prefix != 0) { + if (*sp != *prefix) return false; + sp++; prefix++; + } + return *prefix == 0; +} + #endif diff --git a/src/includes/load-service.h b/src/includes/load-service.h index 21cb463..fcddec0 100644 --- a/src/includes/load-service.h +++ b/src/includes/load-service.h @@ -1,5 +1,47 @@ #include #include +#include +#include + +#include +#include +#include +#include +#include + +struct service_flags_t +{ + // on-start flags: + bool rw_ready : 1; // file system should be writable once this service starts + bool log_ready : 1; // syslog should be available once this service starts + + // Other service options flags: + bool no_sigterm : 1; // do not send SIGTERM + bool runs_on_console : 1; // run "in the foreground" + bool starts_on_console : 1; // starts in the foreground + bool shares_console : 1; // run on console, but not exclusively + bool pass_cs_fd : 1; // pass this service a control socket connection via fd + bool start_interruptible : 1; // the startup of this service process is ok to interrupt with SIGINT + bool skippable : 1; // if interrupted the service is skipped (scripted services) + bool signal_process_only : 1; // signal the session process, not the whole group + + service_flags_t() noexcept : rw_ready(false), log_ready(false), no_sigterm(false), + runs_on_console(false), starts_on_console(false), shares_console(false), + pass_cs_fd(false), start_interruptible(false), skippable(false), signal_process_only(false) + { + } +}; + +// Resource limits for a particular service & particular resource +struct service_rlimits +{ + int resource_id; // RLIMIT_xxx identifying resource + bool soft_set : 1; + bool hard_set : 1; + struct rlimit limits; + + service_rlimits(int id) : resource_id(id), soft_set(0), hard_set(0), limits({0,0}) { } +}; // Exception while loading a service class service_load_exc @@ -47,6 +89,40 @@ namespace dinit_load { using string = std::string; using string_iterator = std::string::iterator; +// A service directory entry, tracking the directory as a nul-terminated string, which may either +// be static or dynamically allocated (via new char[...]). +class dir_entry +{ + const char *dir; + bool dir_dyn_allocd; // dynamically allocated? + + public: + dir_entry(const char *dir_p, bool dir_dyn_allocd_p) : + dir(dir_p), dir_dyn_allocd(dir_dyn_allocd_p) + { } + + dir_entry(dir_entry &&other) + { + dir = other.dir; + dir_dyn_allocd = other.dir_dyn_allocd; + other.dir_dyn_allocd = false; + } + + dir_entry(const dir_entry &other) = delete; + + ~dir_entry() + { + if (dir_dyn_allocd) { + delete[] dir; + } + } + + const char *get_dir() const + { + return dir; + } +}; + // exception thrown when encountering a syntax issue when reading a setting value class setting_exception { @@ -80,6 +156,18 @@ inline string_iterator skipws(string_iterator i, string_iterator end) return i; } +// Convert a signal name to the corresponding signal number +inline int signal_name_to_number(std::string &signame) +{ + if (signame == "HUP") return SIGHUP; + if (signame == "INT") return SIGINT; + if (signame == "QUIT") return SIGQUIT; + if (signame == "USR1") return SIGUSR1; + if (signame == "USR2") return SIGUSR2; + if (signame == "KILL") return SIGKILL; + return -1; +} + // Read a setting name. inline string read_setting_name(string_iterator & i, string_iterator end) { @@ -98,7 +186,7 @@ inline string read_setting_name(string_iterator & i, string_iterator end) return rval; } -// Read a setting value +// Read a setting value. // // In general a setting value is a single-line string. It may contain multiple parts // separated by white space (which is normally collapsed). A hash mark - # - denotes @@ -111,13 +199,14 @@ inline string read_setting_name(string_iterator & i, string_iterator end) // as '#' or '"' or another backslash) to remove its special meaning. Newline // characters are not allowed in values and cannot be quoted. // -// This function expects the string to be in an ASCII-compatible, single byte -// encoding (the "classic" locale). +// This function expects the string to be in an ASCII-compatible encoding (the "classic" locale). +// +// Throws setting_exception on error. // // Params: // service_name - the name of the service to which the setting applies // i - reference to string iterator through the line -// end - iterator at end of line +// end - iterator at end of line (not including newline character if any) // part_positions - list of to which the position of each setting value // part will be added as [start,end). May be null. inline string read_setting_value(string_iterator & i, string_iterator end, @@ -144,19 +233,16 @@ inline string read_setting_value(string_iterator & i, string_iterator end, while (i != end) { c = *i; if (c == '\"') break; - if (c == '\n') { - throw setting_exception("Line end inside quoted string"); - } else if (c == '\\') { // A backslash escapes the following character. ++i; if (i != end) { c = *i; - if (c == '\n') { - throw setting_exception("Line end follows backslash escape character (`\\')"); - } rval += c; } + else { + throw setting_exception("Line end follows backslash escape character (`\\')"); + } } else { rval += c; @@ -216,13 +302,261 @@ inline string read_setting_value(string_iterator & i, string_iterator end, return rval; } +// Parse a userid parameter which may be a numeric user ID or a username. If a name, the +// userid is looked up via the system user database (getpwnam() function). In this case, +// the associated group is stored in the location specified by the group_p parameter iff +// it is not null and iff it contains the value -1. +inline uid_t parse_uid_param(const std::string ¶m, const std::string &service_name, gid_t *group_p) +{ + const char * uid_err_msg = "Specified user id contains invalid numeric characters " + "or is outside allowed range."; + + // Could be a name or a numeric id. But we should assume numeric first, just in case + // a user manages to give themselves a username that parses as a number. + std::size_t ind = 0; + try { + // POSIX does not specify whether uid_t is an signed or unsigned, but regardless + // is is probably safe to assume that valid values are positive. We'll also assert + // that the value range fits within "unsigned long long" since it seems unlikely + // that would ever not be the case. + static_assert((uintmax_t)std::numeric_limits::max() + <= (uintmax_t)std::numeric_limits::max(), "uid_t is too large"); + unsigned long long v = std::stoull(param, &ind, 0); + if (v > static_cast(std::numeric_limits::max()) + || ind != param.length()) { + throw service_description_exc(service_name, uid_err_msg); + } + return v; + } + catch (std::out_of_range &exc) { + throw service_description_exc(service_name, uid_err_msg); + } + catch (std::invalid_argument &exc) { + // Ok, so it doesn't look like a number: proceed... + } + + errno = 0; + struct passwd * pwent = getpwnam(param.c_str()); + if (pwent == nullptr) { + // Maybe an error, maybe just no entry. + if (errno == 0) { + throw service_description_exc(service_name, "Specified user \"" + param + + "\" does not exist in system database."); + } + else { + throw service_description_exc(service_name, std::string("Error accessing user database: ") + + strerror(errno)); + } + } + + if (group_p && *group_p != (gid_t)-1) { + *group_p = pwent->pw_gid; + } + + return pwent->pw_uid; +} + +inline gid_t parse_gid_param(const std::string ¶m, const std::string &service_name) +{ + const char * gid_err_msg = "Specified group id contains invalid numeric characters or is " + "outside allowed range."; + + // Could be a name or a numeric id. But we should assume numeric first, just in case + // a user manages to give themselves a username that parses as a number. + std::size_t ind = 0; + try { + // POSIX does not specify whether uid_t is an signed or unsigned, but regardless + // is is probably safe to assume that valid values are positive. We'll also assume + // that the value range fits with "unsigned long long" since it seems unlikely + // that would ever not be the case. + static_assert((uintmax_t)std::numeric_limits::max() + <= (uintmax_t)std::numeric_limits::max(), "gid_t is too large"); + unsigned long long v = std::stoull(param, &ind, 0); + if (v > static_cast(std::numeric_limits::max()) + || ind != param.length()) { + throw service_description_exc(service_name, gid_err_msg); + } + return v; + } + catch (std::out_of_range &exc) { + throw service_description_exc(service_name, gid_err_msg); + } + catch (std::invalid_argument &exc) { + // Ok, so it doesn't look like a number: proceed... + } + + errno = 0; + struct group * grent = getgrnam(param.c_str()); + if (grent == nullptr) { + // Maybe an error, maybe just no entry. + if (errno == 0) { + throw service_description_exc(service_name, "Specified group \"" + param + + "\" does not exist in system database."); + } + else { + throw service_description_exc(service_name, std::string("Error accessing group database: ") + + strerror(errno)); + } + } + + return grent->gr_gid; +} + +// Parse a time, specified as a decimal number of seconds (with optional fractional component after decimal +// point or decimal comma). +inline void parse_timespec(const std::string ¶mval, const std::string &servicename, + const char * paramname, timespec &ts) +{ + decltype(ts.tv_sec) isec = 0; + decltype(ts.tv_nsec) insec = 0; + auto max_secs = std::numeric_limits::max() / 10; + auto len = paramval.length(); + decltype(len) i; + for (i = 0; i < len; i++) { + char ch = paramval[i]; + if (ch == '.' || ch == ',') { + i++; + break; + } + if (ch < '0' || ch > '9') { + throw service_description_exc(servicename, std::string("Bad value for ") + paramname); + } + // check for overflow + if (isec >= max_secs) { + throw service_description_exc(servicename, std::string("Too-large value for ") + paramname); + } + isec *= 10; + isec += ch - '0'; + } + decltype(insec) insec_m = 100000000; // 10^8 + for ( ; i < len; i++) { + char ch = paramval[i]; + if (ch < '0' || ch > '9') { + throw service_description_exc(servicename, std::string("Bad value for ") + paramname); + } + insec += (ch - '0') * insec_m; + insec_m /= 10; + } + ts.tv_sec = isec; + ts.tv_nsec = insec; +} + +// Parse an unsigned numeric parameter value +inline unsigned long long parse_unum_param(const std::string ¶m, const std::string &service_name, + unsigned long long max = std::numeric_limits::max()) +{ + const char * num_err_msg = "Specified value contains invalid numeric characters or is outside " + "allowed range."; + + std::size_t ind = 0; + try { + unsigned long long v = std::stoull(param, &ind, 0); + if (v > max || ind != param.length()) { + throw service_description_exc(service_name, num_err_msg); + } + return v; + } + catch (std::out_of_range &exc) { + throw service_description_exc(service_name, num_err_msg); + } + catch (std::invalid_argument &exc) { + throw service_description_exc(service_name, num_err_msg); + } +} + +// In a vector, find or create rlimits for a particular resource type. +inline service_rlimits &find_rlimits(std::vector &all_rlimits, int resource_id) +{ + for (service_rlimits &limits : all_rlimits) { + if (limits.resource_id == resource_id) { + return limits; + } + } + + all_rlimits.emplace_back(resource_id); + return all_rlimits.back(); +} + +// Parse resource limits setting (can specify both hard and soft limit). +inline void parse_rlimit(const std::string &line, const std::string &service_name, const char *param_name, + service_rlimits &rlimit) +{ + // Examples: + // 4:5 - soft:hard limits both set + // 4:- soft set, hard set to unlimited + // 4: soft set, hard limit unchanged + // 4 soft and hard limit set to same limit + + if (line.empty()) { + throw service_description_exc(service_name, std::string("Bad value for ") + param_name); + } + + const char *cline = line.c_str(); + rlimit.hard_set = rlimit.soft_set = false; + + try { + const char * index = cline; + errno = 0; + if (cline[0] != ':') { + rlimit.soft_set = true; + if (cline[0] == '-') { + rlimit.limits.rlim_cur = RLIM_INFINITY; + index = cline + 1; + } + else { + char *nindex; + unsigned long long limit = std::strtoull(cline, &nindex, 0); + index = nindex; + if (errno == ERANGE || limit > std::numeric_limits::max()) throw std::out_of_range(""); + if (index == cline) throw std::invalid_argument(""); + rlimit.limits.rlim_cur = limit; + } + + if (*index == 0) { + rlimit.hard_set = true; + rlimit.limits.rlim_max = rlimit.limits.rlim_cur; + return; + } + + if (*index != ':') { + throw service_description_exc(service_name, std::string("Bad value for ") + param_name); + } + } + + index++; + if (*index == 0) return; + + if (*index == '-') { + rlimit.limits.rlim_max = RLIM_INFINITY; + if (index[1] != 0) { + throw service_description_exc(service_name, std::string("Bad value for ") + param_name); + } + } + else { + const char *hard_start = index; + char *nindex; + unsigned long long limit = std::strtoull(cline, &nindex, 0); + index = nindex; + if (errno == ERANGE || limit > std::numeric_limits::max()) throw std::out_of_range(""); + if (index == hard_start) throw std::invalid_argument(""); + rlimit.limits.rlim_max = limit; + } + } + catch (std::invalid_argument &exc) { + throw service_description_exc(service_name, std::string("Bad value for ") + param_name); + } + catch (std::out_of_range &exc) { + throw service_description_exc(service_name, std::string("Too-large value for ") + param_name); + } +} + // Process an opened service file, line by line. // name - the service name // service_file - the service file input stream // func - a function of the form: // void(string &line, string &setting, string_iterator i, string_iterator end) // Called with: -// line - the complete line +// line - the complete line (excluding newline character) // setting - the setting name, from the beginning of the line // i - iterator at the beginning of the setting value // end - iterator marking the end of the line diff --git a/src/includes/proc-service.h b/src/includes/proc-service.h index 085026b..010037a 100644 --- a/src/includes/proc-service.h +++ b/src/includes/proc-service.h @@ -15,16 +15,6 @@ std::vector separate_args(std::string &s, const std::list> &arg_indices); -struct service_rlimits -{ - int resource_id; // RLIMIT_xxx identifying resource - bool soft_set : 1; - bool hard_set : 1; - struct rlimit limits; - - service_rlimits(int id) : resource_id(id), soft_set(0), hard_set(0), limits({0,0}) { } -}; - // Parameters for process execution struct run_proc_params { diff --git a/src/includes/service.h b/src/includes/service.h index 7db1036..e46de70 100644 --- a/src/includes/service.h +++ b/src/includes/service.h @@ -111,29 +111,6 @@ * transition stage, at the latest. */ -struct service_flags_t -{ - // on-start flags: - bool rw_ready : 1; // file system should be writable once this service starts - bool log_ready : 1; // syslog should be available once this service starts - - // Other service options flags: - bool no_sigterm : 1; // do not send SIGTERM - bool runs_on_console : 1; // run "in the foreground" - bool starts_on_console : 1; // starts in the foreground - bool shares_console : 1; // run on console, but not exclusively - bool pass_cs_fd : 1; // pass this service a control socket connection via fd - bool start_interruptible : 1; // the startup of this service process is ok to interrupt with SIGINT - bool skippable : 1; // if interrupted the service is skipped (scripted services) - bool signal_process_only : 1; // signal the session process, not the whole group - - service_flags_t() noexcept : rw_ready(false), log_ready(false), no_sigterm(false), - runs_on_console(false), starts_on_console(false), shares_console(false), - pass_cs_fd(false), start_interruptible(false), skippable(false), signal_process_only(false) - { - } -}; - class service_record; class service_set; class base_process_service; @@ -927,35 +904,12 @@ class service_set } }; -// A service directory entry, tracking the directory as a nul-terminated string, which may either -// be static or dynamically allocated (via new char[...]). -class service_dir_entry -{ - const char *dir; - bool dir_dyn_allocd; // dynamically allocated? - - public: - service_dir_entry(const char *dir_p, bool dir_dyn_allocd_p) : - dir(dir_p), dir_dyn_allocd(dir_dyn_allocd_p) - { } - - ~service_dir_entry() - { - if (dir_dyn_allocd) { - delete[] dir; - } - } - - const char *get_dir() const - { - return dir; - } -}; - // A service set which loads services from one of several service directories. class dirload_service_set : public service_set { - std::vector service_dirs; // directories containing service descriptions + using dir_entry = dinit_load::dir_entry; + + std::vector service_dirs; // directories containing service descriptions public: dirload_service_set() : service_set() diff --git a/src/load-service.cc b/src/load-service.cc index b556588..7453bb3 100644 --- a/src/load-service.cc +++ b/src/load-service.cc @@ -22,266 +22,6 @@ using string = std::string; using string_iterator = std::string::iterator; -// Convert a signal name to the corresponding signal number -static int signal_name_to_number(std::string &signame) -{ - if (signame == "HUP") return SIGHUP; - if (signame == "INT") return SIGINT; - if (signame == "QUIT") return SIGQUIT; - if (signame == "USR1") return SIGUSR1; - if (signame == "USR2") return SIGUSR2; - if (signame == "KILL") return SIGKILL; - return -1; -} - -static const char * uid_err_msg = "Specified user id contains invalid numeric characters " - "or is outside allowed range."; - -// Parse a userid parameter which may be a numeric user ID or a username. If a name, the -// userid is looked up via the system user database (getpwnam() function). In this case, -// the associated group is stored in the location specified by the group_p parameter iff -// it is not null and iff it contains the value -1. -static uid_t parse_uid_param(const std::string ¶m, const std::string &service_name, gid_t *group_p) -{ - // Could be a name or a numeric id. But we should assume numeric first, just in case - // a user manages to give themselves a username that parses as a number. - std::size_t ind = 0; - try { - // POSIX does not specify whether uid_t is an signed or unsigned, but regardless - // is is probably safe to assume that valid values are positive. We'll also assert - // that the value range fits within "unsigned long long" since it seems unlikely - // that would ever not be the case. - static_assert((uintmax_t)std::numeric_limits::max() - <= (uintmax_t)std::numeric_limits::max(), "uid_t is too large"); - unsigned long long v = std::stoull(param, &ind, 0); - if (v > static_cast(std::numeric_limits::max()) - || ind != param.length()) { - throw service_description_exc(service_name, uid_err_msg); - } - return v; - } - catch (std::out_of_range &exc) { - throw service_description_exc(service_name, uid_err_msg); - } - catch (std::invalid_argument &exc) { - // Ok, so it doesn't look like a number: proceed... - } - - errno = 0; - struct passwd * pwent = getpwnam(param.c_str()); - if (pwent == nullptr) { - // Maybe an error, maybe just no entry. - if (errno == 0) { - throw service_description_exc(service_name, "Specified user \"" + param - + "\" does not exist in system database."); - } - else { - throw service_description_exc(service_name, std::string("Error accessing user database: ") - + strerror(errno)); - } - } - - if (group_p && *group_p != (gid_t)-1) { - *group_p = pwent->pw_gid; - } - - return pwent->pw_uid; -} - -static const char * num_err_msg = "Specified value contains invalid numeric characters or is outside " - "allowed range."; - -// Parse an unsigned numeric parameter value -static unsigned long long parse_unum_param(const std::string ¶m, const std::string &service_name, - unsigned long long max = std::numeric_limits::max()) -{ - std::size_t ind = 0; - try { - unsigned long long v = std::stoull(param, &ind, 0); - if (v > max || ind != param.length()) { - throw service_description_exc(service_name, num_err_msg); - } - return v; - } - catch (std::out_of_range &exc) { - throw service_description_exc(service_name, num_err_msg); - } - catch (std::invalid_argument &exc) { - throw service_description_exc(service_name, num_err_msg); - } -} - -static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is " - "outside allowed range."; - -static gid_t parse_gid_param(const std::string ¶m, const std::string &service_name) -{ - // Could be a name or a numeric id. But we should assume numeric first, just in case - // a user manages to give themselves a username that parses as a number. - std::size_t ind = 0; - try { - // POSIX does not specify whether uid_t is an signed or unsigned, but regardless - // is is probably safe to assume that valid values are positive. We'll also assume - // that the value range fits with "unsigned long long" since it seems unlikely - // that would ever not be the case. - static_assert((uintmax_t)std::numeric_limits::max() - <= (uintmax_t)std::numeric_limits::max(), "gid_t is too large"); - unsigned long long v = std::stoull(param, &ind, 0); - if (v > static_cast(std::numeric_limits::max()) - || ind != param.length()) { - throw service_description_exc(service_name, gid_err_msg); - } - return v; - } - catch (std::out_of_range &exc) { - throw service_description_exc(service_name, gid_err_msg); - } - catch (std::invalid_argument &exc) { - // Ok, so it doesn't look like a number: proceed... - } - - errno = 0; - struct group * grent = getgrnam(param.c_str()); - if (grent == nullptr) { - // Maybe an error, maybe just no entry. - if (errno == 0) { - throw service_description_exc(service_name, "Specified group \"" + param - + "\" does not exist in system database."); - } - else { - throw service_description_exc(service_name, std::string("Error accessing group database: ") - + strerror(errno)); - } - } - - return grent->gr_gid; -} - -// Parse a time, specified as a decimal number of seconds (with optional fractional component after decimal -// point or decimal comma). -static void parse_timespec(const std::string ¶mval, const std::string &servicename, - const char * paramname, timespec &ts) -{ - decltype(ts.tv_sec) isec = 0; - decltype(ts.tv_nsec) insec = 0; - auto max_secs = std::numeric_limits::max() / 10; - auto len = paramval.length(); - decltype(len) i; - for (i = 0; i < len; i++) { - char ch = paramval[i]; - if (ch == '.' || ch == ',') { - i++; - break; - } - if (ch < '0' || ch > '9') { - throw service_description_exc(servicename, std::string("Bad value for ") + paramname); - } - // check for overflow - if (isec >= max_secs) { - throw service_description_exc(servicename, std::string("Too-large value for ") + paramname); - } - isec *= 10; - isec += ch - '0'; - } - decltype(insec) insec_m = 100000000; // 10^8 - for ( ; i < len; i++) { - char ch = paramval[i]; - if (ch < '0' || ch > '9') { - throw service_description_exc(servicename, std::string("Bad value for ") + paramname); - } - insec += (ch - '0') * insec_m; - insec_m /= 10; - } - ts.tv_sec = isec; - ts.tv_nsec = insec; -} - -// In a vector, find or create rlimits for a particular resource type. -static service_rlimits &find_rlimits(std::vector &all_rlimits, int resource_id) -{ - for (service_rlimits &limits : all_rlimits) { - if (limits.resource_id == resource_id) { - return limits; - } - } - - all_rlimits.emplace_back(resource_id); - return all_rlimits.back(); -} - -// Parse resource limits setting (can specify both hard and soft limit). -static void parse_rlimit(const std::string &line, const std::string &service_name, const char *param_name, - service_rlimits &rlimit) -{ - // Examples: - // 4:5 - soft:hard limits both set - // 4:- soft set, hard set to unlimited - // 4: soft set, hard limit unchanged - // 4 soft and hard limit set to same limit - - if (line.empty()) { - throw service_description_exc(service_name, std::string("Bad value for ") + param_name); - } - - const char *cline = line.c_str(); - rlimit.hard_set = rlimit.soft_set = false; - - try { - const char * index = cline; - errno = 0; - if (cline[0] != ':') { - rlimit.soft_set = true; - if (cline[0] == '-') { - rlimit.limits.rlim_cur = RLIM_INFINITY; - index = cline + 1; - } - else { - char *nindex; - unsigned long long limit = std::strtoull(cline, &nindex, 0); - index = nindex; - if (errno == ERANGE || limit > std::numeric_limits::max()) throw std::out_of_range(""); - if (index == cline) throw std::invalid_argument(""); - rlimit.limits.rlim_cur = limit; - } - - if (*index == 0) { - rlimit.hard_set = true; - rlimit.limits.rlim_max = rlimit.limits.rlim_cur; - return; - } - - if (*index != ':') { - throw service_description_exc(service_name, std::string("Bad value for ") + param_name); - } - } - - index++; - if (*index == 0) return; - - if (*index == '-') { - rlimit.limits.rlim_max = RLIM_INFINITY; - if (index[1] != 0) { - throw service_description_exc(service_name, std::string("Bad value for ") + param_name); - } - } - else { - const char *hard_start = index; - char *nindex; - unsigned long long limit = std::strtoull(cline, &nindex, 0); - index = nindex; - if (errno == ERANGE || limit > std::numeric_limits::max()) throw std::out_of_range(""); - if (index == hard_start) throw std::invalid_argument(""); - rlimit.limits.rlim_max = limit; - } - } - catch (std::invalid_argument &exc) { - throw service_description_exc(service_name, std::string("Bad value for ") + param_name); - } - catch (std::out_of_range &exc) { - throw service_description_exc(service_name, std::string("Too-large value for ") + param_name); - } -} - // Perform environment variable substitution on a command line, if specified. // line - the string storing the command and arguments // offsets - the [start,end) pair of offsets of the command and each argument within the string @@ -372,17 +112,6 @@ static void process_dep_dir(dirload_service_set &sset, closedir(depdir); } -// Check if one string starts with another -static bool starts_with(string s, const char *prefix) -{ - const char * sp = s.c_str(); - while (*sp != 0 && *prefix != 0) { - if (*sp != *prefix) return false; - sp++; prefix++; - } - return *prefix == 0; -} - // Find a service record, or load it from file. If the service has dependencies, load those also. // // Throws service_load_exc (or subclass) if a dependency cycle is found or if another -- 2.25.1