/* * Copyright (c) 2011-2023 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * init.c * Start the operating system. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // TODO: The Sortix doesn't expose this at the moment. #if !defined(HOST_NAME_MAX) && defined(__sortix__) #include #endif #include #include #include #include #include #include struct device_match { const char* path; struct blockdevice* bdev; }; struct mountpoint { struct fstab entry; char* entry_line; pid_t pid; char* absolute; }; enum verbosity { VERBOSITY_SILENT, VERBOSITY_QUIET, VERBOSITY_VERBOSE, }; enum exit_code_meaning { EXIT_CODE_MEANING_DEFAULT, EXIT_CODE_MEANING_POWEROFF_REBOOT, }; enum daemon_state { // Daemon is not running and should not be running. DAEMON_STATE_TERMINATED, // Daemon is not running but should be scheduled to run. DAEMON_STATE_SCHEDULED, // Daemon is not running but will start after its dependencies are ready. DAEMON_STATE_WAITING, // Daemon is not running and can start now that the dependencies are ready. DAEMON_STATE_SATISFIED, // Daemon is running but isn't ready. DAEMON_STATE_STARTING, // Daemon is running and is ready. DAEMON_STATE_RUNNING, // Daemon is running and is being terminated. DAEMON_STATE_TERMINATING, // Daemon just finished running and the other daemons needs to be notified. DAEMON_STATE_FINISHING, // Daemon has finished running. DAEMON_STATE_FINISHED, }; #define NUM_DAEMON_STATES (DAEMON_STATE_FINISHED + 1) struct daemon; struct dependency { struct daemon* source; struct daemon* target; int flags; }; #define DEPENDENCY_FLAG_REQUIRE (1 << 0) #define DEPENDENCY_FLAG_AWAIT (1 << 1) #define DEPENDENCY_FLAG_EXIT_CODE (1 << 2) enum log_method { LOG_METHOD_NONE, LOG_METHOD_APPEND, LOG_METHOD_ROTATE, }; enum log_format { LOG_FORMAT_NONE, LOG_FORMAT_SECONDS, LOG_FORMAT_NANOSECONDS, LOG_FORMAT_BASIC, LOG_FORMAT_FULL, LOG_FORMAT_SYSLOG, }; struct log { char* name; pid_t pid; enum log_method method; enum log_format format; bool control_messages; bool rotate_on_start; size_t max_rotations; off_t max_line_size; size_t skipped; off_t max_size; char* path; char* path_src; char* path_dst; size_t path_number_offset; size_t path_number_size; char* buffer; size_t buffer_used; size_t buffer_size; off_t size; int fd; int last_errno; bool line_terminated; bool line_begun; mode_t file_mode; }; struct daemon { char* name; struct daemon* next_by_state; struct daemon* prev_by_state; struct daemon* parent_of_parameter; struct daemon* first_by_parameter; struct daemon* last_by_parameter; struct daemon* prev_by_parameter; struct daemon* next_by_parameter; struct dependency** dependencies; size_t dependencies_used; size_t dependencies_length; size_t dependencies_ready; size_t dependencies_finished; size_t dependencies_failed; struct dependency** dependents; size_t dependents_used; size_t dependents_length; size_t reference_count; size_t pfd_readyfd_index; size_t pfd_outputfd_index; struct dependency* exit_code_from; char* cd; char* netif; int argc; char** argv; struct termios oldtio; struct log log; struct timespec timeout; pid_t pid; enum exit_code_meaning exit_code_meaning; enum daemon_state state; int exit_code; int readyfd; int outputfd; bool configured; bool need_tty; bool was_ready; bool was_terminated; bool was_dereferenced; bool timeout_set; }; struct dependency_config { char* target; int flags; }; struct daemon_config { char* name; struct dependency_config** dependencies; size_t dependencies_used; size_t dependencies_length; char* cd; int argc; char** argv; enum exit_code_meaning exit_code_meaning; bool per_if; bool need_tty; enum log_method log_method; enum log_format log_format; bool log_control_messages; bool log_rotate_on_start; size_t log_rotations; off_t log_line_size; off_t log_size; mode_t log_file_mode; }; static pid_t main_pid; static pid_t forward_signal_pid = -1; static volatile sig_atomic_t caught_exit_signal = -1; static sigset_t handled_signals; static struct daemon_config default_config = { .log_method = LOG_METHOD_ROTATE, .log_format = LOG_FORMAT_NANOSECONDS, .log_control_messages = true, .log_rotate_on_start = false, .log_rotations = 3, .log_line_size = 4096, .log_size = 1048576, .log_file_mode = 0644, }; static struct log init_log = { .fd = -1 }; static enum verbosity verbosity = VERBOSITY_QUIET; static struct harddisk** hds = NULL; static size_t hds_used = 0; static size_t hds_length = 0; static struct mountpoint* mountpoints = NULL; static size_t mountpoints_used = 0; static size_t mountpoints_length = 0; static struct daemon** daemons = NULL; static size_t daemons_used = 0; static size_t daemons_length = 0; static struct daemon* first_daemon_by_state[NUM_DAEMON_STATES]; static struct daemon* last_daemon_by_state[NUM_DAEMON_STATES]; static size_t count_daemon_by_state[NUM_DAEMON_STATES]; static struct pollfd* pfds = NULL; static size_t pfds_used = 0; static size_t pfds_length = 0; static struct daemon** pfds_daemon = NULL; static size_t pfds_daemon_length = 0; static bool chain_location_made = false; static char chain_location[] = "/tmp/fs.XXXXXX"; static bool chain_location_dev_made = false; static char chain_location_dev[] = "/tmp/fs.XXXXXX/dev"; static void signal_handler(int signum) { if ( getpid() != main_pid ) return; if ( forward_signal_pid != -1 ) { if ( 0 < forward_signal_pid ) kill(forward_signal_pid, signum); return; } switch ( signum ) { case SIGINT: caught_exit_signal = 1; break; case SIGTERM: caught_exit_signal = 0; break; case SIGQUIT: caught_exit_signal = 2; break; case SIGHUP: caught_exit_signal = 3; break; } } static void install_signal_handler(void) { sigemptyset(&handled_signals); sigaddset(&handled_signals, SIGINT); sigaddset(&handled_signals, SIGQUIT); sigaddset(&handled_signals, SIGTERM); sigaddset(&handled_signals, SIGHUP); sigprocmask(SIG_BLOCK, &handled_signals, NULL); struct sigaction sa = { .sa_handler = signal_handler, .sa_flags = 0 }; sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGHUP, &sa, NULL); } static void uninstall_signal_handler(void) { struct sigaction sa = { .sa_handler = SIG_DFL, .sa_flags = 0 }; sa.sa_handler = SIG_DFL; sa.sa_flags = 0; sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigprocmask(SIG_UNBLOCK, &handled_signals, NULL); } static char* read_single_line(FILE* fp) { char* ret = NULL; size_t ret_size = 0; ssize_t ret_length = getline(&ret, &ret_size, fp); if ( ret_length < 0 ) { free(ret); return NULL; } if ( ret[ret_length-1] == '\n' ) ret[--ret_length] = '\0'; return ret; } static char* join_paths(const char* a, const char* b) { size_t a_len = strlen(a); bool has_slash = (a_len && a[a_len-1] == '/') || b[0] == '/'; char* result; if ( (has_slash && asprintf(&result, "%s%s", a, b) < 0) || (!has_slash && asprintf(&result, "%s/%s", a, b) < 0) ) return NULL; return result; } static bool array_add(void*** array_ptr, size_t* used_ptr, size_t* length_ptr, void* value) { void** array; memcpy(&array, array_ptr, sizeof(array)); // Strict aliasing. if ( *used_ptr == *length_ptr ) { size_t length = *length_ptr; if ( !length ) length = 4; void** new_array = reallocarray(array, length, 2 * sizeof(void*)); if ( !new_array ) return false; array = new_array; memcpy(array_ptr, &array, sizeof(array)); // Strict aliasing. *length_ptr = length * 2; } memcpy(array + (*used_ptr)++, &value, sizeof(value)); // Strict aliasing. return true; } static int exit_code_to_exit_status(int exit_code) { if ( WIFEXITED(exit_code) ) return WEXITSTATUS(exit_code); else if ( WIFSIGNALED(exit_code) ) return 128 + WTERMSIG(exit_code); else return 2; } static void log_close(struct log* log) { if ( 0 <= log->fd ) close(log->fd); log->fd = -1; free(log->buffer); log->buffer = NULL; } static void log_error(struct log* log, const char* prefix, const char* path) { // TODO: Log to the init log unless about the init log. if ( !errno ) warnx("%s%s", prefix, path ? path : log->path); else if ( errno != log->last_errno ) warn("%s%s", prefix, path ? path : log->path); log->last_errno = errno; } static bool log_open(struct log* log) { if ( log->method == LOG_METHOD_NONE ) return true; int logflags = O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW; if ( log->method == LOG_METHOD_APPEND && log->rotate_on_start ) logflags |= O_TRUNC; if ( 0 <= log->fd ) close(log->fd); log->fd = open(log->path, logflags, log->file_mode); if ( log->fd < 0 ) { if ( errno != EROFS ) log_error(log, "", NULL); // Don't block daemon startup on read-only filesystems. return errno == EROFS; } struct stat st; if ( fstat(log->fd, &st) < 0 ) { log_error(log, "stat: ", NULL); close(log->fd); log->fd = -1; return false; } if ( (st.st_mode & 07777) != log->file_mode ) { if ( fchmod(log->fd, log->file_mode) < 0 ) { log_error(log, "fchmod: ", NULL); close(log->fd); log->fd = -1; return false; } } log->size = st.st_size; log->line_terminated = true; return true; } static bool log_rotate(struct log* log) { if ( log->method == LOG_METHOD_NONE ) return true; if ( 0 <= log->fd ) { close(log->fd); log->fd = -1; } for ( size_t i = log->max_rotations; 0 < i; i-- ) { snprintf(log->path_dst + log->path_number_offset, log->path_number_size, ".%zu", i); snprintf(log->path_src + log->path_number_offset, log->path_number_size, "%c%zu", i-1 != 0 ? '.' : '\0', i-1); if ( i == log->max_rotations ) { if ( !access(log->path_dst, F_OK) ) { // Ensure the file system space usage has an upper bound by // deleting the oldest log. However if another process has the // log open, the kernel will keep the file contents alive. The // file is truncated to zero size to avoid disk space remaining // temporarily in use that way, although the inode itself does // remain open temporarily. // TODO: truncateat should have a flags parameter, to not follow // symbolic links. Otherwise a symlink in /var/log could be // used to truncate an arbitrary file, which is avoided here. int fd = open(log->path_dst, O_WRONLY | O_NOFOLLOW); if ( fd < 0 ) { // Don't rotate logs on read-only filesystems. if ( errno == EROFS ) break; log_error(log, "archiving: opening: ", log->path_dst); } else { if ( ftruncate(fd, 0) < 0 ) log_error(log, "archiving: truncate: ", log->path_dst); close(fd); } if ( unlink(log->path_dst) < 0 ) log_error(log, "archiving: unlink: ", log->path_dst); } else if ( errno != ENOENT ) log_error(log, "archiving: ", log->path_dst); } if ( rename(log->path_src, log->path_dst) < 0 ) { // Don't rotate logs on read-only filesystems. if ( errno == EROFS ) break; // Ignore non-existent logs. if ( errno != ENOENT ) { log_error(log, "archiving: ", log->path_src); return errno == EROFS; } } } return log_open(log); } static bool log_initialize(struct log* log, const char* name, struct daemon_config* daemon_config) { memset(log, 0, sizeof(*log)); log->fd = -1; log->method = daemon_config->log_method; log->format = daemon_config->log_format; log->control_messages = daemon_config->log_control_messages; log->rotate_on_start = daemon_config->log_rotate_on_start; log->max_rotations = daemon_config->log_rotations; log->max_line_size = daemon_config->log_line_size; log->max_size = daemon_config->log_size; if ( log->max_size < log->max_line_size ) log->max_line_size = log->max_size; log->file_mode = daemon_config->log_file_mode; log->name = strdup(name); if ( !log->name ) return false; if ( asprintf(&log->path, "/var/log/%s.log", name) < 0 ) return free(log->name), false; // Preallocate the paths used when renaming log files so there's no error // conditions when cycling logs. if ( asprintf(&log->path_src, "%s.%i", log->path, INT_MAX) < 0 ) return free(log->path), free(log->name), false; if ( asprintf(&log->path_dst, "%s.%i", log->path, INT_MAX) < 0 ) return free(log->path_src), free(log->path), free(log->name), false; log->path_number_offset = strlen(log->path); log->path_number_size = strlen(log->path_dst) + 1 - log->path_number_offset; return true; } static bool log_begin_buffer(struct log* log) { log->buffer_used = 0; log->buffer_size = 4096; log->buffer = malloc(log->buffer_size); if ( !log->buffer ) return false; return true; } static void log_data_to_buffer(struct log* log, const char* data, size_t length) { assert(log->buffer); if ( log->skipped ) { log->skipped += length; return; } while ( length ) { size_t available = log->buffer_size - log->buffer_used; if ( !available ) { if ( 1048576 <= log->buffer_size ) { errno = 0; log_error(log, "in-memory buffer exhausted: ", NULL); log->skipped += length; return; } size_t new_size = 2 * log->buffer_size; char* new_buffer = realloc(log->buffer, new_size); if ( !new_buffer ) { log_error(log, "expanding in-memory buffer: ", NULL); log->skipped += length; return; } log->buffer = new_buffer; log->buffer_size = new_size; available = log->buffer_size - log->buffer_used; } size_t amount = length < available ? length : available; memcpy(log->buffer + log->buffer_used, data, amount); data += amount; length -= amount; log->buffer_used += amount; } } static void log_data(struct log* log, const char* data, size_t length) { if ( log->method == LOG_METHOD_NONE ) return; if ( log->fd < 0 && log->buffer ) { log_data_to_buffer(log, data, length); return; } if ( log->skipped ) { // TODO: Try to log a mesage about how many bytes were skipped and // resume logging if that worked. For now, let's just lose data. } const off_t chunk_cut_offset = log->max_size - log->max_line_size; size_t sofar = 0; while ( sofar < length ) { if ( log->fd < 0 ) { log->skipped += length - sofar; return; } // If the data is currently line terminated, then cut if we can't add // another line of the maximum length, otherwise cut if the chunk is // full. if ( log->method == LOG_METHOD_ROTATE && (log->line_terminated ? chunk_cut_offset : log->max_size) <= log->size ) { if ( !log_rotate(log) ) { log->skipped += length - sofar; return; } } // Decide the size of the new chunk to write out. const char* next_data = data + sofar; size_t remaining_length = length - sofar; size_t next_length = remaining_length; if ( log->method == LOG_METHOD_ROTATE ) { off_t chunk_left = log->max_size - log->size; next_length = (uintmax_t) remaining_length < (uintmax_t) chunk_left ? (size_t) remaining_length : (size_t) chunk_left; // Attempt to cut the log at a newline. if ( chunk_cut_offset <= log->size + (off_t) next_length ) { // Find where the data becomes eligible for a line cut, and // search for a newline after that. size_t first_cut_index = log->size < chunk_cut_offset ? 0 : (size_t) (chunk_cut_offset - log->size); for ( size_t i = first_cut_index; i < next_length; i++ ) { if ( next_data[i] == '\n' ) { next_length = i + 1; break; } } } } ssize_t amount = write(log->fd, next_data, next_length); if ( amount < 0 ) { log_error(log, "writing: ", NULL); log->skipped += length - sofar; return; } sofar += amount; log->size += amount; log->line_terminated = next_data[amount - 1] == '\n'; log->last_errno = 0; } } static void log_formatted(struct log* log, const char* string, size_t length) { if ( log->format == LOG_FORMAT_NONE ) { log_data(log, string, length); return; } size_t log_name_length = strlen(log->name); for ( size_t i = 0; i < length; ) { size_t fragment = 1; while ( string[i + fragment - 1] != '\n' && i + fragment < length ) fragment++; if ( !log->line_begun ) { struct timespec now; clock_gettime(CLOCK_REALTIME, &now); struct tm tm; gmtime_r(&now.tv_sec, &tm); char hostname[HOST_NAME_MAX + 1]; gethostname(hostname, sizeof(hostname)); if ( log->format == LOG_FORMAT_SYSLOG ) { int pri = 3 /* system daemons */ * 8 + 6 /* informational */; char header[64]; snprintf(header, sizeof(header), "<%d>1 ", pri); log_data(log, header, strlen(header)); } char timeformat[64] = "%F %T +0000"; if ( log->format == LOG_FORMAT_SYSLOG ) snprintf(timeformat, sizeof(timeformat), "%%FT%%T.%06liZ", now.tv_nsec / 1000); else if ( log->format != LOG_FORMAT_SECONDS ) snprintf(timeformat, sizeof(timeformat), "%%F %%T.%09li +0000", now.tv_nsec); char timestamp[64]; strftime(timestamp, sizeof(timestamp), timeformat, &tm); log_data(log, timestamp, strlen(timestamp)); if ( log->format == LOG_FORMAT_FULL || log->format == LOG_FORMAT_SYSLOG ) { log_data(log, " ", 1); log_data(log, hostname, strlen(hostname)); } if ( log->format == LOG_FORMAT_BASIC || log->format == LOG_FORMAT_FULL || log->format == LOG_FORMAT_SYSLOG ) { log_data(log, " ", 1); log_data(log, log->name, log_name_length); } if ( log->format == LOG_FORMAT_SYSLOG ) { pid_t pid = 0 < log->pid ? log->pid : getpid(); char part[64]; snprintf(part, sizeof(part), " %ji - - ", (intmax_t) pid); log_data(log, part, strlen(part)); } else log_data(log, ": ", 2); } log_data(log, string + i, fragment); log->line_begun = string[i + fragment - 1] != '\n'; i += fragment; } } static size_t log_callback(void* ctx, const char* str, size_t len) { log_formatted((struct log*) ctx, str, len); return len; } static bool log_begin(struct log* log) { if ( log->method == LOG_METHOD_NONE ) return true; bool opened; if ( log->method == LOG_METHOD_ROTATE && log->rotate_on_start ) opened = log_rotate(log); else opened = log_open(log); if ( !opened ) return false; if ( log->buffer ) { log_data(log, log->buffer, log->buffer_used); free(log->buffer); log->buffer = NULL; log->buffer_used = 0; log->buffer_size = 0; // TODO: Warn about any skipped data. log->skipped = 0; } return true; } __attribute__((format(printf, 2, 3))) static void log_status(const char* status, const char* format, ...) { va_list ap; va_start(ap, format); vcbprintf(&init_log, log_callback, format, ap); va_end(ap); if ( verbosity == VERBOSITY_SILENT || (verbosity == VERBOSITY_QUIET && strcmp(status, "failed") != 0 && strcmp(status, "timeout") != 0) ) return; struct timespec now; clock_gettime(CLOCK_REALTIME, &now); struct tm tm; localtime_r(&now.tv_sec, &tm); va_start(ap, format); fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday + 1, tm.tm_hour, tm.tm_min, tm.tm_sec); if ( !strcmp(status, "starting") ) fprintf(stderr, "[ ] "); else if ( !strcmp(status, "started") ) fprintf(stderr, "[ \e[92mOK\e[m ] "); else if ( !strcmp(status, "finished") ) fprintf(stderr, "[ \e[92mDONE\e[m ] "); else if ( !strcmp(status, "failed") ) fprintf(stderr, "[\e[91mFAILED\e[m] "); else if ( !strcmp(status, "stopping") ) fprintf(stderr, "[ ] "); else if ( !strcmp(status, "stopped") ) fprintf(stderr, "[ \e[92mOK\e[m ] "); else if ( !strcmp(status, "timeout") ) fprintf(stderr, "[\e[93m TIME \e[m] "); else fprintf(stderr, "[ ?? ] "); vfprintf(stderr, format, ap); fflush(stderr); va_end(ap); } __attribute__((format(printf, 1, 2))) noreturn static void fatal(const char* format, ...) { va_list ap; va_start(ap, format); fprintf(stderr, "%s: fatal: ", program_invocation_name); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); fflush(stderr); va_end(ap); if ( getpid() == main_pid ) { va_start(ap, format); vcbprintf(&init_log, log_callback, format, ap); log_formatted(&init_log, "\n", 1); va_end(ap); } if ( getpid() == main_pid ) exit(2); _exit(2); } __attribute__((format(printf, 1, 2))) static void warning(const char* format, ...) { va_list ap; va_start(ap, format); fprintf(stderr, "%s: warning: ", program_invocation_name); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); fflush(stderr); va_end(ap); if ( getpid() == main_pid ) { va_start(ap, format); vcbprintf(&init_log, log_callback, format, ap); log_formatted(&init_log, "\n", 1); va_end(ap); } } __attribute__((format(printf, 1, 2))) static void note(const char* format, ...) { va_list ap; va_start(ap, format); fprintf(stderr, "%s: ", program_invocation_name); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); fflush(stderr); va_end(ap); if ( getpid() == main_pid ) { va_start(ap, format); vcbprintf(&init_log, log_callback, format, ap); log_formatted(&init_log, "\n", 1); va_end(ap); } } static char** tokenize(size_t* out_tokens_used, const char* string) { size_t tokens_used = 0; size_t tokens_length = 0; char** tokens = malloc(sizeof(char*)); if ( !tokens ) return NULL; bool failed = false; bool invalid = false; while ( *string ) { if ( isspace((unsigned char) *string) ) { string++; continue; } if ( *string == '#' ) break; char* token; size_t token_size; FILE* fp = open_memstream(&token, &token_size); if ( !fp ) { failed = true; break; } bool singly = false; bool doubly = false; bool escaped = false; for ( char c = *string++; c; c = *string++ ) { if ( !escaped && !singly && !doubly && isspace((unsigned char) c) ) break; if ( !escaped && !doubly && c == '\'' ) { singly = !singly; continue; } if ( !escaped && !singly && c == '"' ) { doubly = !doubly; continue; } if ( !singly && !escaped && c == '\\' ) { escaped = true; continue; } if ( escaped ) { switch ( c ) { case 'a': c = '\a'; break; case 'b': c = '\b'; break; case 'e': c = '\e'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'v': c = '\v'; break; default: break; }; } escaped = false; if ( fputc((unsigned char) c, fp) == EOF ) { failed = true; break; } } if ( singly || doubly || escaped ) { fclose(fp); free(token); invalid = true; break; } if ( fflush(fp) == EOF ) { fclose(fp); free(token); failed = true; break; } fclose(fp); if ( !array_add((void***) &tokens, &tokens_used, &tokens_length, token) ) { free(token); failed = true; break; } } if ( failed || invalid ) { for ( size_t i = 0; i < tokens_used; i++ ) free(tokens[i]); free(tokens); if ( invalid ) errno = 0; return NULL; } char** new_tokens = reallocarray(tokens, tokens_used, sizeof(char*)); if ( new_tokens ) tokens = new_tokens; *out_tokens_used = tokens_used; return tokens; } static void daemon_config_free(struct daemon_config* daemon_config) { free(daemon_config->name); for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) { free(daemon_config->dependencies[i]->target); free(daemon_config->dependencies[i]); } free(daemon_config->dependencies); free(daemon_config->cd); for ( int i = 0; i < daemon_config->argc; i++ ) free(daemon_config->argv[i]); free(daemon_config->argv); free(daemon_config); } static bool daemon_config_load_search(struct daemon_config* daemon_config, size_t next_search_path_index); static bool daemon_process_command(struct daemon_config* daemon_config, const char* path, size_t argc, const char* const* argv, off_t line_number, size_t next_search_path_index) { if ( !argc ) return true; if ( !strcmp(argv[0], "furthermore") ) { if ( 2 <= argc ) warning("%s:%ji: unexpected parameter to %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); // TODO: Only once per search path level. // TODO: How about requiring it to be the first statement? if ( !daemon_config_load_search(daemon_config, next_search_path_index) ) { if ( errno == ENOENT ) { warning("%s:%ji: 'furthermore' failed to locate next '%s' " "configuration file in search path: %m", path, (intmax_t) line_number, daemon_config->name); errno = EINVAL; } else warning("%s: while processing 'furthermore': %m", path); return false; } return true; } if ( argc == 1 ) { warning("%s:%ji: expected parameter: %s", path, (intmax_t) line_number, argv[0]); return false; } if ( !strcmp(argv[0], "cd") ) { free(daemon_config->cd); if ( !(daemon_config->cd = strdup(argv[1])) ) { warning("strdup: %m"); return false; } } else if ( !strcmp(argv[0], "exec") ) { for ( int i = 0; i < daemon_config->argc; i++ ) free(daemon_config->argv[i]); free(daemon_config->argv); daemon_config->argc = 0; daemon_config->argv = NULL; if ( INT_MAX - 1 < argc - 1 ) { warning("%s:%ji: too many arguments: %s", path, (intmax_t) line_number, argv[0]); return false; } int new_argc = argc - 1; char** new_argv = calloc(new_argc + 1, sizeof(char*)); if ( !new_argv ) { warning("malloc: %m"); return false; } for ( int i = 0; i < new_argc; i++ ) { size_t n = 1 + (size_t) i; if ( !(new_argv[i] = strdup(argv[n])) ) { warning("malloc: %m"); for ( int j = 0; i < j; j++ ) free(new_argv[j]); free(new_argv); return false; } } daemon_config->argc = new_argc; daemon_config->argv = new_argv; } else if ( !strcmp(argv[0], "exit-code-meaning") ) { if ( !strcmp(argv[1], "default") ) daemon_config->exit_code_meaning = EXIT_CODE_MEANING_DEFAULT; else if ( !strcmp(argv[1], "poweroff-reboot") ) daemon_config->exit_code_meaning = EXIT_CODE_MEANING_POWEROFF_REBOOT; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "log-control-messages") ) { if ( !strcmp(argv[1], "true") ) daemon_config->log_control_messages = true; else if ( !strcmp(argv[1], "false") ) daemon_config->log_control_messages = false; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "log-file-mode") ) { char* end; errno = 0; uintmax_t value = strtoumax(argv[1], &end, 8); if ( argv[1] == end || errno || value != (value & 07777) ) warning("%s:%ji: invalid %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); else daemon_config->log_file_mode = (mode_t) value; } else if ( !strcmp(argv[0], "log-format") ) { if ( !strcmp(argv[1], "none") ) daemon_config->log_format = LOG_FORMAT_NONE; else if ( !strcmp(argv[1], "seconds") ) daemon_config->log_format = LOG_FORMAT_SECONDS; else if ( !strcmp(argv[1], "nanoseconds") ) daemon_config->log_format = LOG_FORMAT_NANOSECONDS; else if ( !strcmp(argv[1], "basic") ) daemon_config->log_format = LOG_FORMAT_BASIC; else if ( !strcmp(argv[1], "full") ) daemon_config->log_format = LOG_FORMAT_FULL; else if ( !strcmp(argv[1], "syslog") ) daemon_config->log_format = LOG_FORMAT_SYSLOG; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "log-line-size") ) { char* end; errno = 0; intmax_t value = strtoimax(argv[1], &end, 10); if ( argv[1] == end || errno || value != (off_t) value || value < 0 ) warning("%s:%ji: invalid %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); else daemon_config->log_line_size = (off_t) value; } else if ( !strcmp(argv[0], "log-method") ) { if ( !strcmp(argv[1], "append") ) daemon_config->log_method = LOG_METHOD_APPEND; else if ( !strcmp(argv[1], "rotate") ) daemon_config->log_method = LOG_METHOD_ROTATE; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "log-rotate-on-start") ) { if ( !strcmp(argv[1], "true") ) daemon_config->log_rotate_on_start = true; else if ( !strcmp(argv[1], "false") ) daemon_config->log_rotate_on_start = false; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "log-size") ) { char* end; errno = 0; intmax_t value = strtoimax(argv[1], &end, 10); if ( argv[1] == end || errno || value != (off_t) value || value < 0 ) warning("%s:%ji: invalid %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); else daemon_config->log_size = (off_t) value; } else if ( !strcmp(argv[0], "per") ) { if ( !strcmp(argv[1], "if") ) daemon_config->per_if = true; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "need") ) { if ( !strcmp(argv[1], "tty") ) daemon_config->need_tty = true; else warning("%s:%ji: unknown %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); } else if ( !strcmp(argv[0], "require") ) { char* target = strdup(argv[1]); if ( !target ) { warning("strdup: %m"); return false; } int negated_flags = DEPENDENCY_FLAG_REQUIRE | DEPENDENCY_FLAG_AWAIT; int flags = negated_flags; for ( size_t i = 2; i < argc; i++ ) { if ( !strcmp(argv[i], "optional") ) flags &= ~DEPENDENCY_FLAG_REQUIRE; else if ( !strcmp(argv[i], "no-await") ) flags &= ~DEPENDENCY_FLAG_AWAIT; else if ( !strcmp(argv[i], "exit-code") ) flags |= DEPENDENCY_FLAG_EXIT_CODE; else warning("%s:%ji: %s %s: unknown flag: %s", path, (intmax_t) line_number, argv[0], argv[1], argv[i]); } bool had_exit_code = false; struct dependency_config* dependency = NULL; for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) { struct dependency_config* dep = daemon_config->dependencies[i]; if ( dep->flags & DEPENDENCY_FLAG_EXIT_CODE ) had_exit_code = true; if ( !strcmp(dep->target, target) ) dependency = dep; } if ( (flags & DEPENDENCY_FLAG_EXIT_CODE) && had_exit_code ) { warning("%s:%ji: %s %s: exit-code had already been set", path, (intmax_t) line_number, argv[0], argv[1]); flags &= ~DEPENDENCY_FLAG_EXIT_CODE; } if ( dependency ) { dependency->flags &= flags & negated_flags; dependency->flags |= flags & ~negated_flags; free(target); } else { dependency = (struct dependency_config*) calloc(1, sizeof(struct dependency_config)); if ( !dependency ) { warning("malloc: %m"); free(target); return false; } dependency->target = target; dependency->flags = flags; if ( !array_add((void***) &daemon_config->dependencies, &daemon_config->dependencies_used, &daemon_config->dependencies_length, dependency) ) { warning("malloc: %m"); free(target); free(dependency); return false; } } } else if ( !strcmp(argv[0], "tty") ) { // TODO: Implement. } else if ( !strcmp(argv[0], "unset") ) { if ( !strcmp(argv[1], "cd") ) { free(daemon_config->cd); daemon_config->cd = NULL; } else if ( !strcmp(argv[1], "exec") ) { for ( int i = 0; i < daemon_config->argc; i++ ) free(daemon_config->argv[i]); free(daemon_config->argv); daemon_config->argc = 0; daemon_config->argv = NULL; } else if ( !strcmp(argv[1], "exit-code-meaning") ) daemon_config->exit_code_meaning = EXIT_CODE_MEANING_DEFAULT; else if ( !strcmp(argv[1], "log-control-messages") ) daemon_config->log_control_messages = default_config.log_control_messages; else if ( !strcmp(argv[1], "log-file-mode") ) daemon_config->log_file_mode = default_config.log_file_mode; else if ( !strcmp(argv[1], "log-format") ) daemon_config->log_format = default_config.log_format; else if ( !strcmp(argv[1], "log-line-size") ) daemon_config->log_line_size = default_config.log_line_size; else if ( !strcmp(argv[1], "log-method") ) daemon_config->log_method = default_config.log_method; else if ( !strcmp(argv[1], "log-rotate-on-start") ) daemon_config->log_rotate_on_start = default_config.log_rotate_on_start; else if ( !strcmp(argv[1], "log-size") ) daemon_config->log_line_size = default_config.log_line_size; else if ( !strcmp(argv[1], "per") ) { if ( argc < 3 ) warning("%s:%ji: expected parameter: %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); else if ( !strcmp(argv[2], "if") ) daemon_config->per_if = false; else warning("%s:%ji: %s %s: unknown: %s", path, (intmax_t) line_number, argv[0], argv[1], argv[2]); } else if ( !strcmp(argv[1], "need") ) { if ( argc < 3 ) warning("%s:%ji: expected parameter: %s: %s", path, (intmax_t) line_number, argv[0], argv[1]); else if ( !strcmp(argv[2], "tty") ) daemon_config->need_tty = false; else warning("%s:%ji: %s %s: unknown: %s", path, (intmax_t) line_number, argv[0], argv[1], argv[2]); } else if ( !strcmp(argv[1], "require") ) { if ( argc < 3 ) { for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) { free(daemon_config->dependencies[i]->target); free(daemon_config->dependencies[i]); } free(daemon_config->dependencies); daemon_config->dependencies_used = 0; daemon_config->dependencies = NULL; return true; } const char* target = argv[2]; // TODO: Linear time lookup. struct dependency_config* dependency = NULL; size_t i; for ( i = 0; i < daemon_config->dependencies_used; i++ ) { if ( !strcmp(daemon_config->dependencies[i]->target, target) ) { dependency = daemon_config->dependencies[i]; break; } } if ( !dependency ) { warning("%s:%ji: dependency wasn't already required: %s", path, (intmax_t) line_number, target); return true; } if ( argc <= 3 ) { free(daemon_config->dependencies[i]->target); size_t last = daemon_config->dependencies_used - 1; if ( i != last ) { daemon_config->dependencies[i] = daemon_config->dependencies[last]; daemon_config->dependencies[last] = NULL; } daemon_config->dependencies_used--; } else for ( size_t i = 3; i < argc; i++ ) { if ( !strcmp(argv[i], "optional") ) dependency->flags |= DEPENDENCY_FLAG_REQUIRE; else if ( !strcmp(argv[i], "no-await") ) dependency->flags |= DEPENDENCY_FLAG_AWAIT; else if ( !strcmp(argv[i], "exit-code") ) dependency->flags &= ~DEPENDENCY_FLAG_EXIT_CODE; else warning("%s:%ji: %s %s %s: unknown flag: %s", path, (intmax_t) line_number, argv[0], argv[1], argv[2], argv[i]); } } else if ( !strcmp(argv[1], "tty") ) { // TODO: Implement. } else warning("%s:%ji: unknown unset operation: %s", path, (intmax_t) line_number, argv[0]); } else warning("%s:%ji: unknown operation: %s", path, (intmax_t) line_number, argv[0]); return true; } static bool daemon_process_line(struct daemon_config* daemon_config, const char* path, char* line, off_t line_number, size_t next_search_path_index) { size_t argc = 0; char** argv = tokenize(&argc, line); if ( !argv ) { if ( !errno ) warning("%s:%ji: syntax error", path, (intmax_t) line_number); else warning("%s: %m", path); return false; } bool result = daemon_process_command(daemon_config, path, argc, (const char* const*) argv, line_number, next_search_path_index); for ( size_t i = 0; i < argc; i++ ) free(argv[i]); free(argv); return result; } static bool daemon_config_load_from_path(struct daemon_config* daemon_config, const char* path, size_t next_search_path_index) { FILE* fp = fopen(path, "r"); if ( !fp ) { if ( errno != ENOENT ) warning("%s: Failed to open daemon configuration file: %m", path); return false; } char* line = NULL; size_t line_size = 0; ssize_t line_length; off_t line_number = 0; while ( 0 < (line_length = getline(&line, &line_size, fp)) ) { if ( line[line_length-1] == '\n' ) line[--line_length] = '\0'; line_number++; if ( !daemon_process_line(daemon_config, path, line, line_number, next_search_path_index) ) { fclose(fp); free(line); if ( errno == ENOENT ) errno = EINVAL; return false; } } free(line); if ( ferror(fp) ) { warning("%s: %m", path); fclose(fp); return false; } fclose(fp); return true; } static bool daemon_config_load_search(struct daemon_config* daemon_config, size_t next_search_path_index) { // If the search path ever becomes arbitrarily long, consider handling the // 'furthermore' feature in a manner using constant stack space rather than // recursion. const char* search_paths[] = { "/etc/init", "/share/init", }; size_t search_paths_count = sizeof(search_paths) / sizeof(search_paths[0]); for ( size_t i = next_search_path_index; i < search_paths_count; i++ ) { const char* search_path = search_paths[i]; char* path = join_paths(search_path, daemon_config->name); if ( !path ) { warning("malloc: %m"); return false; } if ( !daemon_config_load_from_path(daemon_config, path, i + 1) ) { free(path); if ( errno == ENOENT ) continue; return NULL; } free(path); return true; } errno = ENOENT; return false; } static void daemon_config_initialize(struct daemon_config* daemon_config) { memset(daemon_config, 0, sizeof(*daemon_config)); daemon_config->log_method = default_config.log_method; daemon_config->log_format = default_config.log_format; daemon_config->log_control_messages = default_config.log_control_messages; daemon_config->log_rotate_on_start = default_config.log_rotate_on_start; daemon_config->log_rotations = default_config.log_rotations; daemon_config->log_line_size = default_config.log_line_size; daemon_config->log_size = default_config.log_size; daemon_config->log_file_mode = default_config.log_file_mode; } static struct daemon_config* daemon_config_load(const char* name) { struct daemon_config* daemon_config = malloc(sizeof(struct daemon_config)); if ( !daemon_config ) { warning("malloc: %m"); return NULL; } daemon_config_initialize(daemon_config); if ( !(daemon_config->name = strdup(name)) ) { warning("malloc: %m"); daemon_config_free(daemon_config); return NULL; } if ( !daemon_config_load_search(daemon_config, 0) ) { if ( errno == ENOENT ) warning("Failed to locate daemon configuration: %s: %m", name); daemon_config_free(daemon_config); return NULL; } return daemon_config; } // TODO: Replace with better data structure. static struct daemon* add_daemon(void) { struct daemon* daemon = calloc(1, sizeof(struct daemon)); if ( !daemon ) fatal("malloc: %m"); if ( !array_add((void***) &daemons, &daemons_used, &daemons_length, daemon) ) fatal("malloc: %m"); return daemon; } // TODO: This runs in O(n) but could be in O(log n). static struct daemon* daemon_find_by_name(const char* name) { for ( size_t i = 0; i < daemons_used; i++ ) if ( !strcmp(daemons[i]->name, name) ) return daemons[i]; return NULL; } // TODO: This runs in O(n) but could be in O(log n). static struct daemon* daemon_find_by_pid(pid_t pid) { for ( size_t i = 0; i < daemons_used; i++ ) if ( daemons[i]->pid == pid ) return daemons[i]; return NULL; } static bool daemon_is_failed(struct daemon* daemon) { if ( daemon->was_terminated && WIFSIGNALED(daemon->exit_code) && WTERMSIG(daemon->exit_code) == SIGTERM ) return false; switch ( daemon->exit_code_meaning ) { case EXIT_CODE_MEANING_DEFAULT: return !WIFEXITED(daemon->exit_code) || WEXITSTATUS(daemon->exit_code) != 0; case EXIT_CODE_MEANING_POWEROFF_REBOOT: return !WIFEXITED(daemon->exit_code) || 4 <= WEXITSTATUS(daemon->exit_code); } return true; } static void daemon_insert_state_list(struct daemon* daemon) { assert(!daemon->prev_by_state); assert(!daemon->next_by_state); assert(first_daemon_by_state[daemon->state] != daemon); assert(last_daemon_by_state[daemon->state] != daemon); daemon->prev_by_state = last_daemon_by_state[daemon->state]; daemon->next_by_state = NULL; if ( last_daemon_by_state[daemon->state] ) last_daemon_by_state[daemon->state]->next_by_state = daemon; else first_daemon_by_state[daemon->state] = daemon; last_daemon_by_state[daemon->state] = daemon; count_daemon_by_state[daemon->state]++; } static void daemon_remove_state_list(struct daemon* daemon) { assert(daemon->prev_by_state || daemon == first_daemon_by_state[daemon->state]); assert(daemon->next_by_state || daemon == last_daemon_by_state[daemon->state]); assert(0 < count_daemon_by_state[daemon->state]); if ( daemon->prev_by_state ) daemon->prev_by_state->next_by_state = daemon->next_by_state; else first_daemon_by_state[daemon->state] = daemon->next_by_state; if ( daemon->next_by_state ) daemon->next_by_state->prev_by_state = daemon->prev_by_state; else last_daemon_by_state[daemon->state] = daemon->prev_by_state; count_daemon_by_state[daemon->state]--; daemon->prev_by_state = NULL; daemon->next_by_state = NULL; } static void daemon_change_state_list(struct daemon* daemon, enum daemon_state new_state) { daemon_remove_state_list(daemon); daemon->state = new_state; daemon_insert_state_list(daemon); } static struct daemon* daemon_create_unconfigured(const char* name) { struct daemon* daemon = add_daemon(); if ( !(daemon->name = strdup(name)) ) fatal("malloc: %m"); daemon->state = DAEMON_STATE_TERMINATED; daemon->readyfd = -1; daemon->outputfd = -1; daemon->log.fd = -1; daemon_insert_state_list(daemon); return daemon; } static bool daemon_add_dependency(struct daemon* daemon, struct daemon* target, int flags) { struct dependency* dependency = calloc(1, sizeof(struct dependency)); if ( !dependency ) return false; dependency->source = daemon; dependency->target = target; dependency->flags = flags; if ( !array_add((void***) &daemon->dependencies, &daemon->dependencies_used, &daemon->dependencies_length, dependency) ) { free(dependency); return false; } if ( !array_add((void***) &target->dependents, &target->dependents_used, &target->dependents_length, dependency) ) { daemon->dependencies_used--; free(dependency); return false; } if ( flags & DEPENDENCY_FLAG_EXIT_CODE ) daemon->exit_code_from = dependency; target->reference_count++; return true; } static void daemon_configure_sub(struct daemon* daemon, struct daemon_config* daemon_config, const char* netif) { assert(!daemon->configured); daemon->dependencies = (struct dependency**) reallocarray(NULL, daemon_config->dependencies_used, sizeof(struct dependency*)); if ( !daemon->dependencies ) fatal("malloc: %m"); daemon->dependencies_used = 0; daemon->dependencies_length = daemon_config->dependencies_length; for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) { struct dependency_config* dependency_config = daemon_config->dependencies[i]; struct daemon* target = daemon_find_by_name(dependency_config->target); if ( !target ) target = daemon_create_unconfigured(dependency_config->target); if ( target->netif ) { // daemon_find_by_name cannot create daemons per if. warning("%s cannot depend on parameterized daemon %s", daemon->name, target->name); continue; } if ( !daemon_add_dependency(daemon, target, dependency_config->flags) ) fatal("malloc: %m"); } if ( daemon_config->cd && !(daemon->cd = strdup(daemon_config->cd)) ) fatal("malloc: %m"); if ( daemon_config->argv ) { daemon->argc = daemon_config->argc; if ( netif ) { if ( INT_MAX - 1 <= daemon->argc ) { errno = ENOMEM; fatal("malloc: %m"); } daemon->argc++; } daemon->argv = calloc(daemon->argc + 1, sizeof(char*)); if ( !daemon->argv ) fatal("malloc: %m"); for ( int i = 0; i < daemon_config->argc; i++ ) if ( !(daemon->argv[i] = strdup(daemon_config->argv[i])) ) fatal("malloc: %m"); if ( netif && !(daemon->argv[daemon_config->argc] = strdup(netif)) ) fatal("malloc: %m"); } daemon->exit_code_meaning = daemon_config->exit_code_meaning; if ( netif && !(daemon->netif = strdup(netif)) ) fatal("malloc: %m"); if ( !log_initialize(&daemon->log, daemon->name, daemon_config) ) fatal("malloc: %m"); daemon->need_tty = daemon_config->need_tty; daemon->configured = true; } static void daemon_configure(struct daemon* daemon, struct daemon_config* daemon_config) { if ( daemon_config->per_if ) { struct if_nameindex* ifs = if_nameindex(); if ( !ifs ) fatal("if_nameindex: %m"); for ( size_t i = 0; ifs[i].if_name; i++ ) { const char* netif = ifs[i].if_name; char* parameterized_name; if ( asprintf(¶meterized_name, "%s.%s", daemon_config->name, netif) < 0 ) fatal("malloc: %m"); struct daemon* parameterized = daemon_create_unconfigured(parameterized_name); free(parameterized_name); if ( !(parameterized->netif = strdup(netif)) ) fatal("malloc: %m"); char* name_clone = strdup(parameterized->name); if ( !name_clone ) fatal("malloc: %m"); int flags = DEPENDENCY_FLAG_REQUIRE | DEPENDENCY_FLAG_AWAIT; if ( !daemon_add_dependency(daemon, parameterized, flags) ) fatal("malloc: %m"); daemon_configure_sub(parameterized, daemon_config, netif); } if_freenameindex(ifs); daemon->configured = true; } else daemon_configure_sub(daemon, daemon_config, NULL); } static struct daemon* daemon_create(struct daemon_config* daemon_config) { struct daemon* daemon = daemon_create_unconfigured(daemon_config->name); daemon_configure(daemon, daemon_config); return daemon; } static void schedule_daemon(struct daemon* daemon) { assert(daemon->state == DAEMON_STATE_TERMINATED); daemon_change_state_list(daemon, DAEMON_STATE_SCHEDULED); } static void daemon_on_finished(struct daemon* daemon) { assert(daemon->state != DAEMON_STATE_FINISHING); assert(daemon->state != DAEMON_STATE_FINISHED); if ( daemon_is_failed(daemon) ) log_status("failed", "%s exited unsuccessfully.\n", daemon->name); else if ( daemon->state == DAEMON_STATE_TERMINATING ) log_status("stopped", "Stopped %s.\n", daemon->name); else log_status("finished", "Finished %s.\n", daemon->name); daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); } static void daemon_terminate(struct daemon* daemon) { assert(!daemon->was_terminated); daemon->was_terminated = true; daemon_change_state_list(daemon, DAEMON_STATE_TERMINATING); if ( 0 < daemon->pid ) { log_status("stopping", "Stopping %s.\n", daemon->name); kill(daemon->pid, SIGTERM); struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); daemon->timeout = timespec_add(now, timespec_make(30, 0)); daemon->timeout_set = true; } else daemon_on_finished(daemon); } static void daemon_on_not_referenced(struct daemon* daemon) { assert(daemon->reference_count == 0); switch ( daemon->state ) { case DAEMON_STATE_TERMINATED: case DAEMON_STATE_SCHEDULED: case DAEMON_STATE_WAITING: case DAEMON_STATE_SATISFIED: daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); break; case DAEMON_STATE_STARTING: case DAEMON_STATE_RUNNING: daemon_terminate(daemon); // Dependencies are dereferenced when the daemon terminates. break; case DAEMON_STATE_TERMINATING: case DAEMON_STATE_FINISHING: case DAEMON_STATE_FINISHED: // Dependencies are dereferenced when the daemon terminates. break; } } static void daemon_dereference(struct daemon* daemon) { assert(0 < daemon->reference_count); daemon->reference_count--; if ( !daemon->reference_count ) daemon_on_not_referenced(daemon); } static void daemon_dereference_dependencies(struct daemon* daemon) { assert(!daemon->was_dereferenced); daemon->was_dereferenced = true; for ( size_t i = 0; i < daemon->dependencies_used; i++ ) daemon_dereference(daemon->dependencies[i]->target); } static void daemon_on_dependency_ready(struct dependency* dependency) { struct daemon* daemon = dependency->source; daemon->dependencies_ready++; if ( daemon->state == DAEMON_STATE_WAITING && daemon->dependencies_ready == daemon->dependencies_used ) daemon_change_state_list(daemon, DAEMON_STATE_SATISFIED); } static void daemon_mark_ready(struct daemon* daemon) { daemon_change_state_list(daemon, DAEMON_STATE_RUNNING); daemon->was_ready = true; for ( size_t i = 0; i < daemon->dependents_used; i++ ) { if ( (daemon->dependents[i]->flags & DEPENDENCY_FLAG_AWAIT) ) daemon_on_dependency_ready(daemon->dependents[i]); } } static void daemon_on_ready(struct daemon* daemon) { log_status("started", "Started %s.\n", daemon->name); daemon_mark_ready(daemon); } static void daemon_on_dependency_finished(struct dependency* dependency) { struct daemon* daemon = dependency->source; struct daemon* target = dependency->target; daemon->dependencies_finished++; if ( daemon->state == DAEMON_STATE_FINISHING || daemon->state == DAEMON_STATE_FINISHED ) return; bool failed = (dependency->flags & DEPENDENCY_FLAG_REQUIRE) && daemon_is_failed(target); if ( failed ) daemon->dependencies_failed++; // Don't stop a running non-virtual daemon if dependencies failed. if ( daemon->argv && (daemon->state == DAEMON_STATE_STARTING || daemon->state == DAEMON_STATE_RUNNING || daemon->state == DAEMON_STATE_TERMINATING) ) return; if ( daemon->exit_code_from && (dependency->flags & DEPENDENCY_FLAG_EXIT_CODE) ) { daemon->exit_code = target->exit_code; daemon->exit_code_meaning = target->exit_code_meaning; daemon_on_finished(daemon); return; } if ( failed ) daemon->exit_code = WCONSTRUCT(WNATURE_EXITED, 3, 0); if ( failed || (!daemon->argv && daemon->dependencies_finished == daemon->dependencies_used) ) daemon_on_finished(daemon); } static void daemon_finish(struct daemon* daemon) { assert(daemon->state != DAEMON_STATE_FINISHED); if ( !daemon->was_ready ) daemon_mark_ready(daemon); daemon_change_state_list(daemon, DAEMON_STATE_FINISHED); for ( size_t i = 0; i < daemon->dependents_used; i++ ) daemon_on_dependency_finished(daemon->dependents[i]); daemon_dereference_dependencies(daemon); } static void daemon_on_startup_error(struct daemon* daemon) { assert(daemon->state != DAEMON_STATE_FINISHING); assert(daemon->state != DAEMON_STATE_FINISHED); daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); } static void daemon_register_pollfd(struct daemon* daemon, int fd, size_t* out_index, short events) { assert(pfds_used < pfds_length); assert(pfds_used < pfds_daemon_length); size_t index = pfds_used++; struct pollfd* pfd = pfds + index; memset(pfd, 0, sizeof(*pfd)); pfd->fd = fd; pfd->events = events; pfds_daemon[index] = daemon; *out_index = index; } static void daemon_unregister_pollfd(struct daemon* daemon, size_t index) { assert(pfds_used <= pfds_length); assert(index < pfds_used); assert(pfds_daemon[index] == daemon); // This function is relied on to not mess with any pollfds prior to the // index, so it doesn't break a forward iteration on the pollfds. size_t last_index = pfds_used - 1; if ( index != last_index ) { memcpy(pfds + index, pfds + last_index, sizeof(*pfds)); pfds_daemon[index] = pfds_daemon[last_index]; if ( 0 <= pfds_daemon[index]->readyfd && pfds_daemon[index]->pfd_readyfd_index == last_index ) pfds_daemon[index]->pfd_readyfd_index = index; if ( 0 <= pfds_daemon[index]->outputfd && pfds_daemon[index]->pfd_outputfd_index == last_index ) pfds_daemon[index]->pfd_outputfd_index = index; } pfds_used--; memset(pfds + last_index, 0, sizeof(*pfds)); pfds_daemon[last_index] = NULL; } static void daemon_wait(struct daemon* daemon) { assert(daemon->state == DAEMON_STATE_SCHEDULED); if ( !daemon->configured ) { struct daemon_config* daemon_config = daemon_config_load(daemon->name); if ( !daemon_config ) { log_status("failed", "Failed to load configuration for %s.\n", daemon->name); daemon->exit_code = WCONSTRUCT(WNATURE_EXITED, 3, 0); daemon_on_startup_error(daemon); return NULL; } daemon_configure(daemon, daemon_config); daemon_config_free(daemon_config); } if ( daemon->dependencies_ready == daemon->dependencies_used ) daemon_change_state_list(daemon, DAEMON_STATE_SATISFIED); else daemon_change_state_list(daemon, DAEMON_STATE_WAITING); for ( size_t i = 0; i < daemon->dependencies_used; i++ ) { // TODO: Require the dependency graph to be an directed acylic graph. struct dependency* dependency = daemon->dependencies[i]; assert(dependency->source == daemon); assert(dependency->target); switch ( dependency->target->state ) { case DAEMON_STATE_TERMINATED: schedule_daemon(dependency->target); if ( !(dependency->flags & DEPENDENCY_FLAG_AWAIT) ) daemon_on_dependency_ready(dependency); break; case DAEMON_STATE_SCHEDULED: case DAEMON_STATE_WAITING: case DAEMON_STATE_SATISFIED: case DAEMON_STATE_STARTING: if ( !(dependency->flags & DEPENDENCY_FLAG_AWAIT) ) daemon_on_dependency_ready(dependency); break; case DAEMON_STATE_RUNNING: case DAEMON_STATE_TERMINATING: case DAEMON_STATE_FINISHING: daemon_on_dependency_ready(dependency); break; case DAEMON_STATE_FINISHED: daemon_on_dependency_ready(dependency); daemon_on_dependency_finished(dependency); break; } if ( daemon->state != DAEMON_STATE_WAITING ) break; } } noreturn static void exit_errfd(int errfd, const char* action) { int errnum = errno; write(errfd, &errnum, sizeof(errnum)); write(errfd, action, strlen(action)); _exit(127); } static void daemon_start(struct daemon* daemon) { assert(daemon->state == DAEMON_STATE_SATISFIED); if ( !daemon->argv ) { daemon_on_ready(daemon); if ( daemon->exit_code_from ) { struct daemon* target = daemon->exit_code_from->target; if ( target->state == DAEMON_STATE_FINISHED ) { daemon->exit_code = target->exit_code; daemon->exit_code_meaning = target->exit_code_meaning; daemon_on_finished(daemon); } } else if ( daemon->dependencies_finished == daemon->dependencies_used ) { daemon_on_finished(daemon); } return; } if ( 0 < daemon->dependencies_failed ) { log_status("failed", "Failed to start %s due to failed dependencies.\n", daemon->name); daemon->exit_code = WCONSTRUCT(WNATURE_EXITED, 3, 0); daemon_on_startup_error(daemon); return; } log_status("starting", "Starting %s...\n", daemon->name); uid_t uid = getuid(); pid_t ppid = getpid(); struct passwd* pwd = getpwuid(uid); if ( !pwd ) fatal("looking up user by uid %" PRIuUID ": %m", uid); const char* home = pwd->pw_dir[0] ? pwd->pw_dir : "/"; const char* shell = pwd->pw_shell[0] ? pwd->pw_shell : "sh"; const char* cd = daemon->cd ? daemon->cd : "/"; // TODO: This is a hack. if ( !strcmp(cd, "$HOME") ) cd = home; int outputfds[2]; int readyfds[2]; if ( !daemon->need_tty ) { size_t required_fds = 2; if ( pfds_length - pfds_used < required_fds ) { size_t old_length = pfds_length ? pfds_length : required_fds; struct pollfd* new_pfds = reallocarray(pfds, old_length, 2 * sizeof(struct pollfd)); if ( !new_pfds ) fatal("malloc"); pfds = new_pfds; pfds_length = old_length * 2; } if ( pfds_daemon_length - pfds_used < required_fds ) { size_t old_length = pfds_daemon_length ? pfds_daemon_length : required_fds; struct daemon** new_pfds_daemon = reallocarray(pfds_daemon, old_length, 2 * sizeof(struct daemon*)); if ( !new_pfds_daemon ) fatal("malloc"); pfds_daemon = new_pfds_daemon; pfds_daemon_length = old_length * 2; } if ( !log_begin(&daemon->log) ) { // TODO: Mode where daemons are stopped if logging fails. } if ( pipe(outputfds) < 0 ) fatal("pipe"); daemon->outputfd = outputfds[0]; fcntl(daemon->outputfd, F_SETFL, O_NONBLOCK); // Setup the pollfd for the outputfd. daemon_register_pollfd(daemon, daemon->outputfd, &daemon->pfd_outputfd_index, POLLIN); // Create the readyfd. if ( pipe(readyfds) < 0 ) fatal("pipe"); daemon->readyfd = readyfds[0]; fcntl(daemon->readyfd, F_SETFL, O_NONBLOCK); // Setup the pollfd for the readyfd. daemon_register_pollfd(daemon, daemon->readyfd, &daemon->pfd_readyfd_index, POLLIN); } // TODO: This is not concurrency safe, build a environment array just for // this daemon. char ppid_str[sizeof(pid_t) * 3]; snprintf(ppid_str, sizeof(ppid_str), "%" PRIiPID, ppid); if ( (!daemon->need_tty && setenv("READYFD", "3", 1)) < 0 || setenv("INIT_PID", ppid_str, 1) < 0 || setenv("LOGNAME", pwd->pw_name, 1) < 0 || setenv("USER", pwd->pw_name, 1) < 0 || setenv("HOME", home, 1) < 0 || setenv("SHELL", shell, 1) < 0 ) fatal("setenv"); int errfds[2]; if ( pipe2(errfds, O_CLOEXEC) < 0 ) fatal("pipe"); daemon->pid = daemon->log.pid = fork(); if ( daemon->pid < 0 ) fatal("fork: %m"); if ( daemon->need_tty ) { if ( tcgetattr(0, &daemon->oldtio) ) fatal("tcgetattr: %m"); } if ( daemon->pid == 0 ) { uninstall_signal_handler(); close(errfds[0]); if ( chdir(cd) < 0 ) exit_errfd(errfds[1], "chdir"); if ( daemon->need_tty ) { pid_t pid = getpid(); // TODO: Support for setsid(2). if ( setpgid(0, 0) < 0 ) exit_errfd(errfds[1], "setpgid"); sigset_t oldset, sigttou; sigemptyset(&sigttou); sigaddset(&sigttou, SIGTTOU); sigprocmask(SIG_BLOCK, &sigttou, &oldset); if ( tcsetpgrp(0, pid) < 0 ) exit_errfd(errfds[1], "tcsetpgrp"); daemon->oldtio.c_cflag |= CREAD; if ( tcsetattr(0, TCSANOW, &daemon->oldtio) < 0 ) exit_errfd(errfds[1], "tcsetattr"); sigprocmask(SIG_SETMASK, &oldset, NULL); dup3(errfds[1], 3, O_CLOEXEC); closefrom(4); } else { close(0); close(1); close(2); if ( open("/dev/null", O_RDONLY) < 0 ) exit_errfd(errfds[1], "open"); // The lowest available file descriptor is always allocated, and // because outputfds is allocated first, then readyfds, and finally // errfds, then it's safe to move them downwards in that order. dup2(outputfds[1], 1); dup2(outputfds[1], 2); dup2(readyfds[1], 3); dup3(errfds[1], 4, O_CLOEXEC); closefrom(5); } // TODO: This is a hack. if ( !strcmp(daemon->argv[0], "$SHELL") ) daemon->argv[0] = (char*) shell; execvp(daemon->argv[0], daemon->argv); exit_errfd(4, "execve"); } if ( !daemon->need_tty ) { close(outputfds[1]); close(readyfds[1]); } close(errfds[1]); int errnum; if ( read(errfds[0], &errnum, sizeof(errnum)) == sizeof(errnum) ) { char action[16] = ""; ssize_t amount = read(errfds[0], action, sizeof(action) - 1); if ( 0 <= amount ) action[amount] = '\0'; errno = errnum; // TODO: Write control messages to the daemon log. if ( !strcmp(action, "chdir") ) warning("Failed to start %s: %s: %s: %m", daemon->name, action, cd); else if ( !strcmp(action, "open") ) warning("Failed to start %s: %s: %s: %m", daemon->name, action, "/dev/null"); else if ( !strcmp(action, "execve") ) warning("Failed to start %s: %s: %s: %m", daemon->name, action, daemon->argv[0]); else warning("Failed to start %s: %s: %m", daemon->name, action); } close(errfds[0]); // TODO: Not thread safe. // TODO: Also unset other things. if ( !daemon->need_tty ) unsetenv("READYFD"); unsetenv("INIT_PID"); unsetenv("LOGNAME"); unsetenv("USER"); unsetenv("HOME"); unsetenv("SHELL"); if ( daemon->need_tty ) daemon_on_ready(daemon); else daemon_change_state_list(daemon, DAEMON_STATE_STARTING); } static bool daemon_process_ready(struct daemon* daemon) { char c; ssize_t amount = read(daemon->readyfd, &c, sizeof(c)); if ( amount < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) ) return true; if ( amount < 0 ) return false; else if ( amount == 0 ) return false; if ( c == '\n' ) { daemon_on_ready(daemon); return false; } return true; } static bool daemon_process_output(struct daemon* daemon) { char data[4096]; ssize_t amount = read(daemon->outputfd, data, sizeof(data)); if ( amount < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) ) return true; if ( amount < 0 ) return false; else if ( amount == 0 ) return false; log_formatted(&daemon->log, data, amount); return true; } static void daemon_on_exit(struct daemon* daemon, int exit_code) { assert(daemon->state != DAEMON_STATE_FINISHING); assert(daemon->state != DAEMON_STATE_FINISHED); daemon->exit_code = exit_code; if ( 0 <= daemon->readyfd ) { daemon_unregister_pollfd(daemon, daemon->pfd_readyfd_index); close(daemon->readyfd); daemon->readyfd = -1; } if ( 0 <= daemon->outputfd ) { daemon_process_output(daemon); daemon_unregister_pollfd(daemon, daemon->pfd_outputfd_index); close(daemon->outputfd); daemon->outputfd = -1; } if ( 0 <= daemon->log.fd ) log_close(&daemon->log); if ( daemon->need_tty ) { // TODO: There is a race condition between getting the exit code from // waitpid and us reclaiming it here where some other process that // happened to get the right pid may own the tty. sigset_t oldset, sigttou; sigemptyset(&sigttou); sigaddset(&sigttou, SIGTTOU); sigprocmask(SIG_BLOCK, &sigttou, &oldset); if ( tcsetattr(0, TCSAFLUSH, &daemon->oldtio) ) fatal("tcsetattr: %m"); if ( tcsetpgrp(0, getpgid(0)) < 0 ) fatal("tcsetpgrp: %m"); sigprocmask(SIG_SETMASK, &oldset, NULL); } daemon_on_finished(daemon); } static void init(void) { int default_daemon_exit_code = -1; while ( true ) { if ( caught_exit_signal != -1 && default_daemon_exit_code == -1) { struct daemon* default_daemon = daemon_find_by_name("default"); if ( caught_exit_signal == 0 ) log_status("stopped", "Powering off...\n"); else if ( caught_exit_signal == 1 ) log_status("stopped", "Rebooting...\n"); else if ( caught_exit_signal == 2 ) log_status("stopped", "Halting...\n"); else if ( caught_exit_signal == 3 ) log_status("stopped", "Reinitializing...\n"); else log_status("stopped", "Exiting %i...\n", caught_exit_signal); if ( default_daemon->state != DAEMON_STATE_FINISHING && default_daemon->state != DAEMON_STATE_FINISHED ) daemon_change_state_list(default_daemon, DAEMON_STATE_FINISHING); default_daemon_exit_code = WCONSTRUCT(WNATURE_EXITED, caught_exit_signal, 0); } caught_exit_signal = -1; while ( first_daemon_by_state[DAEMON_STATE_SCHEDULED] || first_daemon_by_state[DAEMON_STATE_SATISFIED] || first_daemon_by_state[DAEMON_STATE_FINISHING] ) { struct daemon* daemon; while ( (daemon = first_daemon_by_state[DAEMON_STATE_SCHEDULED]) ) daemon_wait(daemon); while ( (daemon = first_daemon_by_state[DAEMON_STATE_SATISFIED]) ) daemon_start(daemon); while ( (daemon = first_daemon_by_state[DAEMON_STATE_FINISHING]) ) daemon_finish(daemon); } if ( !first_daemon_by_state[DAEMON_STATE_STARTING] && !first_daemon_by_state[DAEMON_STATE_RUNNING] && !first_daemon_by_state[DAEMON_STATE_TERMINATING] ) break; struct timespec timeout = timespec_make(-1, 0); struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); for ( struct daemon* daemon = first_daemon_by_state[DAEMON_STATE_TERMINATING]; daemon; daemon = daemon->next_by_state ) { if ( !daemon->timeout_set ) continue; if ( timespec_le(daemon->timeout, now) ) { log_status("timeout", "Stopping %s timed out, sending SIGKILL.\n", daemon->name); kill(daemon->pid, SIGKILL); daemon->timeout_set = false; } else { struct timespec left = timespec_sub(daemon->timeout, now); if ( timeout.tv_sec < 0 || timespec_lt(left, timeout) ) timeout = left; } } // Block SIGCHLD so the signal will be delivered during poll(2). sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGCHLD); sigset_t oldset; sigprocmask(SIG_BLOCK, &mask, &oldset); sigset_t unhandled_signals; signotset(&unhandled_signals, &handled_signals); sigset_t pollset; sigandset(&pollset, &oldset, &unhandled_signals); int exit_code; pid_t pid; while ( 0 < (pid = waitpid(-1, &exit_code, WNOHANG)) ) { struct daemon* daemon = daemon_find_by_pid(pid); if ( daemon ) daemon_on_exit(daemon, exit_code); timeout = timespec_make(0, 0); } // Set a dummy SIGCHLD handler to ensure we get EINTR during ppoll(2). struct sigaction sa = { 0 }; sa.sa_handler = signal_handler; sa.sa_flags = 0; struct sigaction old_sa; sigaction(SIGCHLD, &sa, &old_sa); // Await either an event, a timeout, or SIGCHLD. int nevents = ppoll(pfds, pfds_used, &timeout, &pollset); sigaction(SIGCHLD, &old_sa, NULL); sigprocmask(SIG_SETMASK, &oldset, NULL); if ( nevents < 0 && errno != EINTR ) fatal("ppoll: %m"); for ( size_t i = 0; i < pfds_used; i++ ) { if ( nevents <= 0 ) break; struct pollfd* pfd = pfds + i; if ( !pfd->revents ) continue; nevents--; struct daemon* daemon = pfds_daemon[i]; if ( 0 <= daemon->readyfd && pfd->fd == daemon->readyfd ) { if ( pfd->revents & (POLLIN | POLLHUP) ) { if ( !daemon_process_ready(daemon) ) { daemon_unregister_pollfd(daemon, daemon->pfd_readyfd_index); close(daemon->readyfd); daemon->readyfd = -1; i--; // Process this index again (something new there). } } } else if ( 0 <= daemon->outputfd && pfd->fd == daemon->outputfd ) { if ( pfd->revents & (POLLIN | POLLHUP) ) { if ( !daemon_process_output(daemon) ) { daemon_unregister_pollfd(daemon, daemon->pfd_outputfd_index); close(daemon->outputfd); daemon->outputfd = -1; i--; // Process this index again (something new there). } } } else { assert(false); } } } // Collect child processes reparented to us that we don't know about and // attempt to politely shut them down with SIGTERM and SIGKILL after a // timeout. sigset_t saved_mask, sigchld_mask; sigemptyset(&sigchld_mask); sigaddset(&sigchld_mask, SIGCHLD); sigprocmask(SIG_BLOCK, &sigchld_mask, &saved_mask); struct sigaction sa = { .sa_handler = signal_handler }; struct sigaction old_sa; sigaction(SIGCHLD, &sa, &old_sa); struct timespec timeout = timespec_make(30, 0); struct timespec begun; clock_gettime(CLOCK_MONOTONIC, &begun); bool sent_sigterm = false; while ( true ) { int exit_code; for ( pid_t pid = 1; 0 < pid; pid = waitpid(-1, &exit_code, WNOHANG) ); struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); struct timespec elapsed = timespec_sub(now, begun); struct psctl_stat psst; psctl(getpid(), PSCTL_STAT, &psst); bool any_unknown = false; for ( pid_t pid = psst.ppid_first; pid != -1; pid = psst.ppid_next ) { if ( psctl(pid, PSCTL_STAT, &psst) < 0 ) { warning("psctl: %ji", (intmax_t) pid); continue; } bool known = false; for ( size_t i = 0; !known && i < mountpoints_used; i++ ) if ( mountpoints[i].pid == pid ) known = true; if ( !known ) { any_unknown = true; if ( !sent_sigterm ) kill(pid, SIGTERM); // TODO: Hostile processes can try to escape by spawning more // processes, a kernel feature is needed to recursively // send a signal to all descendants atomically, although // want to avoid known safe processes (mountpoints) and // still catch processes reparented to us. Otherwise // retrying until we succeed is the best we can do. else if ( timespec_le(timeout, elapsed) ) kill(pid, SIGKILL); } } sent_sigterm = true; if ( !any_unknown ) break; // Wait for the timeout to happen, or for another process to exit by // the poll failing with EINTR because a pending SIGCHLD was delivered // when the saved signal mask is restored. struct timespec left = timespec_sub(timeout, elapsed); if ( left.tv_sec < 0 || (left.tv_sec == 0 && left.tv_nsec == 0) ) left = timespec_make(1, 0); struct pollfd pfd = { .fd = -1 }; ppoll(&pfd, 1, &left, &saved_mask); } sigaction(SIGCHLD, &old_sa, NULL); sigprocmask(SIG_SETMASK, &saved_mask, NULL); if ( default_daemon_exit_code != -1 ) { struct daemon* default_daemon = daemon_find_by_name("default"); default_daemon->exit_code = default_daemon_exit_code; default_daemon->exit_code_meaning = EXIT_CODE_MEANING_POWEROFF_REBOOT; } } static void write_random_seed(void) { const char* will_not = "next boot will not have fresh randomness"; const char* path = "/boot/random.seed"; int fd = open(path, O_RDWR | O_CREAT | O_NOFOLLOW, 0600); if ( fd < 0 ) { if ( errno != ENOENT && errno != EROFS ) warning("%s: %s: %m", will_not, path); return; } if ( fchown(fd, 0, 0) < 0 ) { warning("%s: chown: %s: %m", will_not, path); close(fd); return; } if ( fchmod(fd, 0600) < 0 ) { warning("%s: chown: %s: %m", will_not, path); close(fd); return; } // Mix in the old random seed so the sysadmin can add new randomness here. unsigned char old[256] = {0}; readall(fd, old, sizeof(old)); lseek(fd, 0, SEEK_SET); // Write out randomness, but mix in some fresh kernel randomness in case the // randomness used to seed arc4random didn't have enough entropy, there may // be more now. unsigned char buf[256]; arc4random_buf(buf, sizeof(buf)); unsigned char newbuf[256]; getentropy(newbuf, sizeof(newbuf)); for ( size_t i = 0; i < 256; i++ ) buf[i] ^= newbuf[i] ^ old[i]; size_t done = writeall(fd, buf, sizeof(buf)); explicit_bzero(buf, sizeof(buf)); if ( done < sizeof(buf) ) { warning("%s: write: %s: %m", will_not, path); close(fd); return; } if ( ftruncate(fd, sizeof(buf)) < 0 ) { warning("%s: truncate: %s: %m", will_not, path); close(fd); return; } close(fd); } static void prepare_filesystem(const char* path, struct blockdevice* bdev) { enum filesystem_error fserr = blockdevice_inspect_filesystem(&bdev->fs, bdev); if ( fserr == FILESYSTEM_ERROR_ABSENT || fserr == FILESYSTEM_ERROR_UNRECOGNIZED ) return; if ( fserr != FILESYSTEM_ERROR_NONE ) return warning("probing: %s: %s", path, filesystem_error_string(fserr)); } static bool prepare_block_device(void* ctx, const char* path) { (void) ctx; struct harddisk* hd = harddisk_openat(AT_FDCWD, path, O_RDONLY); if ( !hd ) { int true_errno = errno; struct stat st; if ( lstat(path, &st) == 0 && !S_ISBLK(st.st_mode) ) return true; errno = true_errno; fatal("%s: %m", path); } if ( !harddisk_inspect_blockdevice(hd) ) { if ( errno == ENOTBLK || errno == ENOMEDIUM ) return true; if ( errno == EINVAL ) return warning("%s: %m", path), true; fatal("%s: %m", path); } if ( hds_used == hds_length ) { size_t new_half_length = hds_length ? hds_length : 8; struct harddisk** new_hds = (struct harddisk**) reallocarray(hds, new_half_length, sizeof(struct harddisk*) * 2); if ( !new_hds ) fatal("realloc: %m"); hds = new_hds; hds_length = 2 * new_half_length; } hds[hds_used++] = hd; struct blockdevice* bdev = &hd->bdev; enum partition_error parterr = blockdevice_get_partition_table(&bdev->pt, bdev); if ( parterr == PARTITION_ERROR_ABSENT || parterr == PARTITION_ERROR_UNRECOGNIZED ) { prepare_filesystem(path, bdev); return true; } else if ( parterr == PARTITION_ERROR_ERRNO ) { if ( errno == EIO || errno == EINVAL ) warning("%s: %s", path, partition_error_string(parterr)); else fatal("%s: %s", path, partition_error_string(parterr)); return true; } else if ( parterr != PARTITION_ERROR_NONE ) { warning("%s: %s", path, partition_error_string(parterr)); return true; } for ( size_t i = 0; i < bdev->pt->partitions_count; i++ ) { struct partition* p = bdev->pt->partitions[i]; assert(p->path); struct stat st; if ( stat(p->path, &st) == 0 ) { // TODO: Check the existing partition has the right offset and // length, but definitely do not recreate it if it already // exists properly. } else if ( errno == ENOENT ) { int mountfd = open(p->path, O_RDONLY | O_CREAT | O_EXCL); if ( mountfd < 0 ) fatal("%s:˙%m", p->path); int partfd = mkpartition(hd->fd, p->start, p->length); if ( partfd < 0 ) fatal("mkpartition: %s:˙%m", p->path); if ( fsm_fsbind(partfd, mountfd, 0) < 0 ) fatal("fsbind: %s:˙%m", p->path); close(partfd); close(mountfd); } else { fatal("stat: %s: %m", p->path); } prepare_filesystem(p->path, &p->bdev); } return true; } static void prepare_block_devices(void) { static bool done = false; if ( done ) return; done = true; if ( !devices_iterate_path(prepare_block_device, NULL) ) fatal("iterating devices: %m"); } static void search_by_uuid(const char* uuid_string, void (*cb)(void*, struct device_match*), void* ctx) { unsigned char uuid[16]; uuid_from_string(uuid, uuid_string); for ( size_t i = 0; i < hds_used; i++ ) { struct blockdevice* bdev = &hds[i]->bdev; if ( bdev->fs ) { struct filesystem* fs = bdev->fs; if ( !(fs->flags & FILESYSTEM_FLAG_UUID) ) continue; if ( memcmp(uuid, fs->uuid, 16) != 0 ) continue; struct device_match match; match.path = hds[i]->path; match.bdev = bdev; cb(ctx, &match); } else if ( bdev->pt ) { for ( size_t j = 0; j < bdev->pt->partitions_count; j++ ) { struct partition* p = bdev->pt->partitions[j]; if ( !p->bdev.fs ) continue; struct filesystem* fs = p->bdev.fs; if ( !(fs->flags & FILESYSTEM_FLAG_UUID) ) continue; if ( memcmp(uuid, fs->uuid, 16) != 0 ) continue; struct device_match match; match.path = p->path; match.bdev = &p->bdev; cb(ctx, &match); } } } } static void ensure_single_device_match(void* ctx, struct device_match* match) { struct device_match* result = (struct device_match*) ctx; if ( result->path ) { if ( result->bdev ) note("duplicate match: %s", result->path); result->bdev = NULL; note("duplicate match: %s", match->path); return; } *result = *match; } static int sort_mountpoint(const void* a_ptr, const void* b_ptr) { const struct mountpoint* a = (const struct mountpoint*) a_ptr; const struct mountpoint* b = (const struct mountpoint*) b_ptr; return strcmp(a->entry.fs_file, b->entry.fs_file); } static void load_fstab(void) { FILE* fp = fopen("/etc/fstab", "r"); if ( !fp ) { if ( errno == ENOENT ) return; fatal("/etc/fstab: %m"); } char* line = NULL; size_t line_size; ssize_t line_length; while ( 0 < (line_length = getline(&line, &line_size, fp)) ) { if ( line[line_length - 1] == '\n' ) line[--line_length] = '\0'; struct fstab fstabent; if ( !scanfsent(line, &fstabent) ) continue; if ( mountpoints_used == mountpoints_length ) { size_t new_length = 2 * mountpoints_length; if ( !new_length ) new_length = 16; struct mountpoint* new_mountpoints = (struct mountpoint*) reallocarray(mountpoints, new_length, sizeof(struct mountpoint)); if ( !new_mountpoints ) fatal("malloc: %m"); mountpoints = new_mountpoints; mountpoints_length = new_length; } struct mountpoint* mountpoint = &mountpoints[mountpoints_used++]; memcpy(&mountpoint->entry, &fstabent, sizeof(fstabent)); mountpoint->entry_line = line; mountpoint->pid = -1; if ( !(mountpoint->absolute = strdup(mountpoint->entry.fs_file)) ) fatal("malloc: %m"); line = NULL; line_size = 0; } if ( ferror(fp) ) fatal("/etc/fstab: %m"); free(line); fclose(fp); qsort(mountpoints, mountpoints_used, sizeof(struct mountpoint), sort_mountpoint); } static void set_hostname(void) { FILE* fp = fopen("/etc/hostname", "r"); if ( !fp && errno == ENOENT ) return; if ( !fp ) return warning("unable to set hostname: /etc/hostname: %m"); char* hostname = read_single_line(fp); fclose(fp); if ( !hostname ) return warning("unable to set hostname: /etc/hostname: %m"); int ret = sethostname(hostname, strlen(hostname)); if ( ret < 0 ) warning("unable to set hostname: `%s': %m", hostname); free(hostname); } static void set_kblayout(void) { int tty_fd = open("/dev/tty", O_RDWR); if ( !tty_fd ) return warning("unable to set keyboard layout: /dev/tty: %m"); bool unsupported = tcgetblob(tty_fd, "kblayout", NULL, 0) < 0 && (errno == ENOTTY || errno == ENOENT); close(tty_fd); if ( unsupported ) return; FILE* fp = fopen("/etc/kblayout", "r"); if ( !fp && errno == ENOENT ) return; if ( !fp ) return warning("unable to set keyboard layout: /etc/kblayout: %m"); char* kblayout = read_single_line(fp); fclose(fp); if ( !kblayout ) return warning("unable to set keyboard layout: /etc/kblayout: %m"); pid_t child_pid = fork(); if ( child_pid < 0 ) { free(kblayout); warning("unable to set keyboard layout: fork: %m"); return; } if ( !child_pid ) { uninstall_signal_handler(); execlp("chkblayout", "chkblayout", "--", kblayout, (const char*) NULL); warning("setting keyboard layout: chkblayout: %m"); _exit(127); } int status; waitpid(child_pid, &status, 0); free(kblayout); } static void set_videomode(void) { int tty_fd = open("/dev/tty", O_RDWR); if ( !tty_fd ) return warning("unable to set video mode: /dev/tty: %m"); struct tiocgdisplay display; struct tiocgdisplays gdisplays; memset(&gdisplays, 0, sizeof(gdisplays)); gdisplays.count = 1; gdisplays.displays = &display; bool unsupported = ioctl(tty_fd, TIOCGDISPLAYS, &gdisplays) < 0 || gdisplays.count == 0; close(tty_fd); if ( unsupported ) return; FILE* fp = fopen("/etc/videomode", "r"); if ( !fp && errno == ENOENT ) return; if ( !fp ) return warning("unable to set video mode: /etc/videomode: %m"); char* videomode = read_single_line(fp); fclose(fp); if ( !videomode ) return warning("unable to set video mode: /etc/videomode: %m"); unsigned int xres = 0; unsigned int yres = 0; unsigned int bpp = 0; if ( sscanf(videomode, "%ux%ux%u", &xres, &yres, &bpp) != 3 ) { warning("/etc/videomode: Invalid video mode `%s'", videomode); free(videomode); return; } free(videomode); struct dispmsg_get_crtc_mode get_mode; memset(&get_mode, 0, sizeof(get_mode)); get_mode.msgid = DISPMSG_GET_CRTC_MODE; get_mode.device = display.device; get_mode.connector = display.connector; // Don't set the resolution if it's already correct. if ( dispmsg_issue(&get_mode, sizeof(get_mode)) == 0 ) { if ( get_mode.mode.control & DISPMSG_CONTROL_VALID && !(get_mode.mode.control & DISPMSG_CONTROL_FALLBACK) && get_mode.mode.fb_format == bpp && get_mode.mode.view_xres == xres && get_mode.mode.view_yres == yres ) return; } struct dispmsg_set_crtc_mode set_mode; memset(&set_mode, 0, sizeof(set_mode)); set_mode.msgid = DISPMSG_SET_CRTC_MODE; set_mode.device = 0; set_mode.connector = 0; set_mode.mode.driver_index = 0; set_mode.mode.magic = 0; set_mode.mode.control = DISPMSG_CONTROL_VALID; set_mode.mode.fb_format = bpp; set_mode.mode.view_xres = xres; set_mode.mode.view_yres = yres; set_mode.mode.fb_location = 0; set_mode.mode.pitch = xres * (bpp / 8); set_mode.mode.surf_off_x = 0; set_mode.mode.surf_off_y = 0; set_mode.mode.start_x = 0; set_mode.mode.start_y = 0; set_mode.mode.end_x = 0; set_mode.mode.end_y = 0; set_mode.mode.desktop_height = yres; if ( dispmsg_issue(&set_mode, sizeof(set_mode)) < 0 ) warning("/etc/videomode: Failed to set video mode `%ux%ux%u': %m", xres, yres, bpp); } static int no_dot_nor_dot_dot(const struct dirent* entry, void* ctx) { (void) ctx; return !strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..") ? 0 : 1; } struct clean_tmp { struct clean_tmp* parent; DIR* dir; char* path; struct dirent** entries; int num_entries; int current_entry; }; static void clean_tmp(const char* tmp_path) { struct clean_tmp* state = calloc(1, sizeof(struct clean_tmp)); if ( !state ) { warning("malloc: %m"); return; } state->path = strdup(tmp_path); if ( !state->path ) { warning("malloc: %m"); free(state); return; } state->dir = opendir(state->path); if ( !state->dir ) { warning("%s: %m", state->path); free(state->path); free(state); return; } while ( state ) { if ( !state->entries ) { state->num_entries = dscandir_r(state->dir, &state->entries, no_dot_nor_dot_dot, NULL, alphasort_r, NULL); if ( state->num_entries < 0 ) warning("%s: %m", state->path); } if ( state->num_entries <= state->current_entry ) { closedir(state->dir); for ( int i = 0; i < state->num_entries; i++ ) free(state->entries[i]); free(state->entries); free(state->path); struct clean_tmp* new_state = state->parent; free(state); state = new_state; if ( state ) { struct dirent* entry = state->entries[state->current_entry]; const char* name = entry->d_name; int fd = dirfd(state->dir); if ( unlinkat(fd, name, AT_REMOVEDIR) < 0 ) warning("%s/%s: %m", state->path, name); state->current_entry++; } continue; } struct dirent* entry = state->entries[state->current_entry]; const char* name = entry->d_name; int fd = dirfd(state->dir); if ( unlinkat(fd, name, AT_REMOVEFILE | AT_REMOVEDIR) < 0 ) { if ( errno == ENOTEMPTY ) { struct clean_tmp* new_state = calloc(1, sizeof(struct clean_tmp)); if ( !new_state ) { warning("%s/%s: malloc: %m", state->path, name); state->current_entry++; continue; } new_state->path = join_paths(state->path, name); if ( !new_state->path ) { warning("%s/%s: malloc: %m", state->path, name); free(new_state); state->current_entry++; continue; } int flags = O_DIRECTORY | O_RDONLY | O_NOFOLLOW; int newfd = openat(fd, new_state->path, flags); if ( newfd < 0 ) { warning("%s: %m", new_state->path); free(new_state->path); free(new_state); state->current_entry++; continue; } new_state->dir = fdopendir(newfd); if ( !new_state->dir ) { warning("%s: %m", new_state->path); close(newfd); free(new_state->path); free(new_state); state->current_entry++; continue; } new_state->parent = state; state = new_state; continue; } else warning("%s/%s: %m", state->path, name); } state->current_entry++; } } static bool fsck(struct filesystem* fs) { struct blockdevice* bdev = fs->bdev; const char* bdev_path = bdev->p ? bdev->p->path : bdev->hd->path; assert(bdev_path); assert(fs->fsck); if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) note("%s: Repairing filesystem due to inconsistency...", bdev_path); else note("%s: Checking filesystem consistency...", bdev_path); pid_t pid = fork(); if ( pid < 0 ) { if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) warning("%s: Mandatory repair failed: fork: %m", bdev_path); else warning("%s: Skipping filesystem check: fork: %m:", bdev_path); return false; } if ( pid == 0 ) { uninstall_signal_handler(); execlp(fs->fsck, fs->fsck, "-fp", "--", bdev_path, (const char*) NULL); warning("%s: Failed to load filesystem checker: %s: %m", bdev_path, fs->fsck); _exit(127); } int code; if ( waitpid(pid, &code, 0) < 0 ) warning("%s: Filesystem check: waitpid: %m", bdev_path); else if ( WIFEXITED(code) && (WEXITSTATUS(code) == 0 || WEXITSTATUS(code) == 1) ) { // Successfully checked filesystem. fs->flags &= ~(FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST); return true; } else if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) { if ( WIFSIGNALED(code) ) warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, strsignal(WTERMSIG(code))); else if ( !WIFEXITED(code) ) warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, "Unexpected unusual termination"); else if ( WEXITSTATUS(code) == 127 ) warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, "Filesystem checker is absent"); else if ( WEXITSTATUS(code) & 2 ) warning("%s: Mandatory repair: %s: %s", bdev_path, fs->fsck, "System reboot is necessary"); else warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, "Filesystem checker was unsuccessful"); } else { if ( WIFSIGNALED(code) ) warning("%s: Filesystem check failed: %s: %s", bdev_path, fs->fsck, strsignal(WTERMSIG(code))); else if ( !WIFEXITED(code) ) warning("%s: Filesystem check failed: %s: %s", bdev_path, fs->fsck, "Unexpected unusual termination"); else if ( WEXITSTATUS(code) == 127 ) warning("%s: Skipping filesystem check: %s: %s", bdev_path, fs->fsck, "Filesystem checker is absent"); else if ( WEXITSTATUS(code) & 2 ) warning("%s: Filesystem check: %s: %s", bdev_path, fs->fsck, "System reboot is necessary"); else warning("%s: Filesystem check failed: %s: %s", bdev_path, fs->fsck, "Filesystem checker was unsuccessful"); } return false; } static bool is_chain_init_mountpoint(const struct mountpoint* mountpoint) { return !strcmp(mountpoint->entry.fs_file, "/"); } static struct filesystem* mountpoint_lookup(const struct mountpoint* mountpoint) { const char* path = mountpoint->entry.fs_file; const char* spec = mountpoint->entry.fs_spec; if ( strncmp(spec, "UUID=", strlen("UUID=")) == 0 ) { const char* uuid = spec + strlen("UUID="); if ( !uuid_validate(uuid) ) { warning("%s: `%s' is not a valid uuid", path, uuid); return NULL; } struct device_match match; memset(&match, 0, sizeof(match)); search_by_uuid(uuid, ensure_single_device_match, &match); if ( !match.path || !match.bdev ) { warning("%s: No devices matching uuid %s were found", path, uuid); return NULL; } assert(match.bdev->fs); return match.bdev->fs; } // TODO: Lookup by device name. warning("%s: Don't know how to resolve `%s' to a filesystem", path, spec); return NULL; } static bool mountpoint_mount(struct mountpoint* mountpoint) { struct filesystem* fs = mountpoint_lookup(mountpoint); if ( !fs ) return false; // TODO: It would be ideal to get an exclusive lock so that no other // processes have currently mounted that filesystem. struct blockdevice* bdev = fs->bdev; const char* bdev_path = bdev->p ? bdev->p->path : bdev->hd->path; assert(bdev_path); const char* pretend_where = mountpoint->entry.fs_file; const char* where = mountpoint->absolute; const char* read_only = NULL; if ( fs->flags & (FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST) ) { if ( !fsck(fs) && (fs->flags & FILESYSTEM_FLAG_FSCK_MUST) ) { warning("Mounting inconsistent filesystem %s read-only on %s", bdev_path, pretend_where); read_only = "-r"; } } if ( !fs->driver ) { warning("Failed mounting %s on %s: " "Don't know how to mount a %s filesystem", bdev_path, pretend_where, fs->fstype_name); return false; } struct stat st; if ( stat(where, &st) < 0 ) { warning("Failed mounting %s on %s: stat: %s: %m", bdev_path, pretend_where, where); return false; } int readyfds[2]; if ( pipe(readyfds) < 0 ) { warning("Failed mounting %s on %s: pipe: %m", bdev_path, pretend_where); return false; } if ( (mountpoint->pid = fork()) < 0 ) { warning("Failed mounting %s on %s: fork: %m", bdev_path, pretend_where); close(readyfds[0]); close(readyfds[1]); return false; } if ( mountpoint->pid == 0 ) { uninstall_signal_handler(); close(readyfds[0]); char readyfdstr[sizeof(int) * 3]; snprintf(readyfdstr, sizeof(readyfdstr), "%d", readyfds[1]); if ( setenv("READYFD", readyfdstr, 1) < 0 ) { warning("Failed mounting %s on %s: setenv: %m", bdev_path, pretend_where); _exit(127); } execlp(fs->driver, fs->driver, "--foreground", bdev_path, where, "--pretend-mount-path", pretend_where, read_only, (const char*) NULL); warning("Failed mount %s on %s: execvp: %s: %m", bdev_path, pretend_where, fs->driver); _exit(127); } close(readyfds[1]); char c; struct stat newst; ssize_t amount = read(readyfds[0], &c, 1); close(readyfds[0]); if ( 0 <= amount ) { if ( !stat(where, &newst) ) { if ( newst.st_dev != st.st_dev || newst.st_ino != st.st_ino ) return true; else warning("Failed mount %s on %s: %s: " "No mounted filesystem appeared: %s", bdev_path, pretend_where, fs->driver, where); } else warning("Failed mounting %s on %s: %s, stat: %s: %m", bdev_path, pretend_where, fs->driver, where); } else warning("Failed mounting %s on %s: %s, Failed to read readiness: %m", bdev_path, pretend_where, fs->driver); if ( unmount(where, 0) < 0 ) { if ( errno != ENOMOUNT ) warning("Failed mounting %s on %s: unmount: %s: %m", bdev_path, pretend_where, where); kill(mountpoint->pid, SIGQUIT); } int code; pid_t child = waitpid(mountpoint->pid, &code, 0); mountpoint->pid = -1; if ( child < 0 ) warning("Failed mounting %s on %s: %s: waitpid: %m", bdev_path, pretend_where, fs->driver); else if ( WIFSIGNALED(code) ) warning("Failed mounting %s on %s: %s: %s", bdev_path, pretend_where, fs->driver, strsignal(WTERMSIG(code))); else if ( !WIFEXITED(code) ) warning("Failed mounting %s on %s: %s: Unexpected unusual termination", bdev_path, pretend_where, fs->driver); else if ( WEXITSTATUS(code) == 127 ) warning("Failed mounting %s on %s: %s: " "Filesystem driver could not be executed", bdev_path, pretend_where, fs->driver); else if ( WEXITSTATUS(code) == 0 ) warning("Failed mounting %s on %s: %s: Unexpected successful exit", bdev_path, pretend_where, fs->driver); else warning("Failed mounting %s on %s: %s: Exited with status %i", bdev_path, pretend_where, fs->driver, WEXITSTATUS(code)); return false; } static void mountpoints_mount(bool is_chain_init) { for ( size_t i = 0; i < mountpoints_used; i++ ) { struct mountpoint* mountpoint = &mountpoints[i]; if ( is_chain_init_mountpoint(mountpoint) != is_chain_init ) continue; mountpoint_mount(mountpoint); } } static void mountpoints_unmount(void) { for ( size_t n = mountpoints_used; n != 0; n-- ) { size_t i = n - 1; struct mountpoint* mountpoint = &mountpoints[i]; if ( mountpoint->pid < 0 ) continue; if ( unmount(mountpoint->absolute, 0) < 0 && errno != ENOMOUNT ) warning("unmount: %s: %m", mountpoint->entry.fs_file); else if ( errno == ENOMOUNT ) kill(mountpoint->pid, SIGTERM); int code; if ( waitpid(mountpoint->pid, &code, 0) < 0 ) note("waitpid: %m"); mountpoint->pid = -1; } } // This function must be usable as an atexit handler, which means it is // undefined behavior for it to invoke exit(), including through calls to fatal // in any function transitively called by this function. static void niht(void) { if ( getpid() != main_pid ) return; // TODO: Unify with new daemon system? At least it needs to recursively kill // all processes. Ideally fatal wouldn't be called for daemons. // TODO: Don't do this unless all the mountpoints were mounted (not for // chain init). write_random_seed(); // Stop logging when unmounting the filesystems. cbprintf(&init_log, log_callback, "Finished operating system.\n"); log_close(&init_log); if ( chain_location_dev_made ) { unmount(chain_location_dev, 0); chain_location_dev_made = false; } mountpoints_unmount(); if ( chain_location_made ) { rmdir(chain_location); chain_location_made = false; } } static void reinit(void) { niht(); const char* argv[] = { "init", NULL }; execv("/sbin/init", (char* const*) argv); fatal("Failed to load init during reinit: %s: %m", argv[0]); } int main(int argc, char* argv[]) { main_pid = getpid(); setlocale(LC_ALL, ""); const char* target_name = "default"; const struct option longopts[] = { {"target", required_argument, NULL, 't'}, {"quiet", no_argument, NULL, 'q'}, {"silent", no_argument, NULL, 's'}, {"verbose", no_argument, NULL, 'v'}, {0, 0, 0, 0} }; const char* opts = "t:qsv"; int opt; while ( (opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1 ) { switch ( opt ) { case 't': target_name = optarg; break; case 'q': verbosity = VERBOSITY_QUIET; break; case 's': verbosity = VERBOSITY_SILENT; break; case 'v': verbosity = VERBOSITY_VERBOSE; break; default: return 2; } } // Prevent recursive init without care. if ( getenv("INIT_PID") ) fatal("System is already managed by an init process"); // Register handler that shuts down the system when init exits. if ( atexit(niht) != 0 ) fatal("atexit: %m"); // Handle signals but block them until the safe points where we handle them. // All child processes have to uninstall the signal handler and unblock the // signals or they keep blocking the signals. install_signal_handler(); // The default daemon brings up the operating system and supplies default // values for the other daemon configuration files. struct daemon_config* default_daemon_config = NULL; if ( !access("/etc/init/default", F_OK) || errno != ENOENT ) { if ( !(default_daemon_config = daemon_config_load("default")) ) fatal("Failed to load default daemon configuration"); } // The default daemon must be configured if no target is specified. if ( !default_daemon_config && !strcmp(target_name, "default") ) fatal("No default daemon configuration was found"); // Daemons inherit their default settings from the default daemon. Load its // configuration (if it exists) even if another default target has been set. if ( default_daemon_config ) { default_config.log_method = default_daemon_config->log_method; default_config.log_format = default_daemon_config->log_format; default_config.log_control_messages = default_daemon_config->log_control_messages; default_config.log_rotate_on_start = default_daemon_config->log_rotate_on_start; default_config.log_rotations = default_daemon_config->log_rotations; default_config.log_line_size = default_daemon_config->log_line_size; default_config.log_size = default_daemon_config->log_size; } // If another daemon has been specified as the boot target, create a fake // default daemon that depends on the specified boot target daemon. if ( strcmp(target_name, "default") != 0 ) { if ( default_daemon_config ) daemon_config_free(default_daemon_config); default_daemon_config = malloc(sizeof(struct daemon_config)); if ( !default_daemon_config ) fatal("malloc: %m"); daemon_config_initialize(default_daemon_config); if ( !(default_daemon_config->name = strdup("default")) ) fatal("malloc: %m"); struct dependency_config* dependency_config = calloc(1, sizeof(struct dependency_config)); if ( !dependency_config ) fatal("malloc: %m"); if ( !(dependency_config->target = strdup(target_name)) ) fatal("malloc: %m"); dependency_config->flags = DEPENDENCY_FLAG_REQUIRE | DEPENDENCY_FLAG_AWAIT | DEPENDENCY_FLAG_EXIT_CODE; if ( !array_add((void***) &default_daemon_config->dependencies, &default_daemon_config->dependencies_used, &default_daemon_config->dependencies_length, dependency_config) ) fatal("malloc: %m"); } else if ( !default_daemon_config ) fatal("Failed to load /etc/init/default: %m"); // Instantiate the default daemon from its configuration. struct daemon* default_daemon = daemon_create(default_daemon_config); daemon_config_free(default_daemon_config); // The default daemon should depend on exactly one top level daemon. const char* first_requirement = 1 <= default_daemon->dependencies_used ? default_daemon->dependencies[0]->target->name : ""; // Log to memory until the log directory has been mounted. if ( !log_initialize(&init_log, "init", &default_config) ) fatal("malloc: %m"); if ( !log_begin_buffer(&init_log) ) fatal("malloc: %m"); init_log.pid = getpid(); cbprintf(&init_log, log_callback, "Initializing operating system...\n"); // Make sure that we have a /tmp directory. umask(0000); mkdir("/tmp", 01777); clean_tmp("/tmp"); // Make sure that we have a /var/run directory. umask(0000); mkdir("/var", 0755); mkdir("/var/run", 0755); clean_tmp("/var/run"); // Set the default file creation mask. umask(0022); // Set up the PATH variable. if ( setenv("PATH", "/bin:/sbin", 1) < 0 ) fatal("setenv: %m"); // Load partition tables and create all the block devices. prepare_block_devices(); // Load the filesystem table. load_fstab(); // If the default daemon's top level dependency is a chain boot target, then // chain boot the actual root filesystem. if ( !strcmp(first_requirement, "chain") || !strcmp(first_requirement, "chain-merge") ) { int next_argc = argc - optind; char** next_argv = argv + optind; // Create a temporary directory where the real root filesystem will be // mounted. if ( !mkdtemp(chain_location) ) fatal("mkdtemp: /tmp/fs.XXXXXX: %m"); chain_location_made = true; // Rewrite the filesystem table to mount inside the temporary directory. bool found_root = false; for ( size_t i = 0; i < mountpoints_used; i++ ) { struct mountpoint* mountpoint = &mountpoints[i]; if ( !strcmp(mountpoint->entry.fs_file, "/") ) found_root = true; char* absolute = join_paths(chain_location, mountpoint->absolute); if ( !absolute ) fatal("malloc: %m"); free(mountpoint->absolute); mountpoint->absolute = absolute; } if ( !found_root ) fatal("/etc/fstab: Root filesystem not found in filesystem table"); // Mount the filesystem table entries marked for chain boot. mountpoints_mount(true); // Additionally bind the /dev filesystem inside the root filesystem. snprintf(chain_location_dev, sizeof(chain_location_dev), "%s/dev", chain_location); if ( mkdir(chain_location_dev, 0755) < 0 && errno != EEXIST && errno != EROFS ) fatal("mkdir: %s: %m", chain_location_dev); int old_dev_fd = open("/dev", O_DIRECTORY | O_RDONLY); if ( old_dev_fd < 0 ) fatal("%s: %m", "/dev"); int new_dev_fd = open(chain_location_dev, O_DIRECTORY | O_RDONLY); if ( new_dev_fd < 0 ) fatal("%s: %m", chain_location_dev); if ( fsm_fsbind(old_dev_fd, new_dev_fd, 0) < 0 ) fatal("mount: `%s' onto `%s': %m", "/dev", chain_location_dev); chain_location_dev_made = true; close(new_dev_fd); close(old_dev_fd); // TODO: Forward the early init log to the chain init. // Run the chain booted operating system. pid_t child_pid = fork(); if ( child_pid < 0 ) fatal("fork: %m"); if ( !child_pid ) { uninstall_signal_handler(); if ( chroot(chain_location) < 0 ) fatal("chroot: %s: %m", chain_location); if ( chdir("/") < 0 ) fatal("chdir: %s: %m", chain_location); const char* program = next_argv[0]; char verbose_opt[] = {'-', "sqv"[verbosity], '\0'}; // Chain boot the operating system upgrade if needed. if ( !strcmp(first_requirement, "chain-merge") ) { program = "/sysmerge/sbin/init"; // TODO: Concat next_argv onto this argv_next, so the arguments // can be passed to the final init. next_argv = (char*[]) { (char*) program, "--target=merge", verbose_opt, NULL }; } else if ( next_argc < 1 ) { program = "/sbin/init"; next_argv = (char*[]) { "init", verbose_opt, NULL }; } execvp(program, (char* const*) next_argv); fatal("Failed to chain load init: %s: %m", next_argv[0]); } forward_signal_pid = child_pid; sigprocmask(SIG_UNBLOCK, &handled_signals, NULL); int status; while ( waitpid(child_pid, &status, 0) < 0 ) { if ( errno != EINTR ) fatal("waitpid: %m"); } sigprocmask(SIG_BLOCK, &handled_signals, NULL); forward_signal_pid = -1; // Racy with waitpid. if ( WIFEXITED(status) ) return WEXITSTATUS(status); else if ( WIFSIGNALED(status) ) fatal("Chain booted init failed with signal: %s", strsignal(WTERMSIG(status))); else fatal("Chain booted init failed unusually"); } // Mount the filesystems, except for the filesystems that would have been // mounted by the chain init. mountpoints_mount(false); // TODO: After releasing Sortix 1.1, remove this compatibility since a // sysmerge from 1.0 will not have a /var/log directory. if ( !strcmp(first_requirement, "merge") && access("/var/log", F_OK) < 0 ) mkdir("/var/log", 0755); // Logging works now that the filesystems have been mounted. Reopen the init // log and write the contents buffered up in memory. log_begin(&init_log); // Update the random seed in case the system fails before it can be written // out during the system shutdown. write_random_seed(); set_hostname(); set_kblayout(); set_videomode(); // Run the operating system upgrade if requested. if ( !strcmp(first_requirement, "merge") ) { pid_t child_pid = fork(); if ( child_pid < 0 ) fatal("fork: %m"); if ( !child_pid ) { uninstall_signal_handler(); const char* argv[] = { "sysmerge", "--booting", NULL }; execv("/sysmerge/sbin/sysmerge", (char* const*) argv); fatal("Failed to load system upgrade: %s: %m", argv[0]); } forward_signal_pid = child_pid; sigprocmask(SIG_UNBLOCK, &handled_signals, NULL); int status; while ( waitpid(child_pid, &status, 0) < 0 ) { if ( errno != EINTR ) fatal("waitpid: %m"); } sigprocmask(SIG_BLOCK, &handled_signals, NULL); forward_signal_pid = -1; // Racy with waitpid. if ( WIFEXITED(status) && WEXITSTATUS(status) != 0 ) fatal("Automatic upgrade failed: Exit status %i", WEXITSTATUS(status)); else if ( WIFSIGNALED(status) ) fatal("Automatic upgrade failed: %s", strsignal(WTERMSIG(status))); else if ( !WIFEXITED(status) ) fatal("Automatic upgrade failed: Unexpected unusual termination"); // Soft reinit into the freshly upgraded operating system. // TODO: Use next_argv here. reinit(); } // TODO: Use the arguments to specify additional things the default daemon // should depend on, as well as a denylist of things not to start // even if in default's dependencies. The easiest thing is probably to // be able to inject require and unset require lines into default. // Request the default daemon be run. schedule_daemon(default_daemon); // Initialize the operating system. init(); // Reinitialize the operating system if requested. if ( default_daemon->exit_code_meaning == EXIT_CODE_MEANING_POWEROFF_REBOOT && WIFEXITED(default_daemon->exit_code) && WEXITSTATUS(default_daemon->exit_code) == 3 ) reinit(); // Finish with the exit code of the default daemon. return exit_code_to_exit_status(default_daemon->exit_code); }