/*******************************************************************************
Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see .
init.c++
Start the operating system.
*******************************************************************************/
#define __STDC_CONSTANT_MACROS
#define __STDC_LIMIT_MACROS
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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 )
return NULL;
if ( ret_length && ret[ret_length-1] == '\n' )
ret[--ret_length] = '\0';
return ret;
}
char* print_string(const char* format, ...)
{
char* ret = NULL;
va_list ap;
va_start(ap, format);
int status = vasprintf(&ret, format, ap);
va_end(ap);
assert(0 <= status);
return ret;
}
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] == '/';
return has_slash ? print_string("%s%s", a, b)
: print_string("%s/%s", a, b);
}
typedef struct
{
char** strings;
size_t length;
size_t capacity;
} string_array_t;
string_array_t string_array_make()
{
string_array_t sa;
sa.strings = NULL;
sa.length = sa.capacity = 0;
return sa;
}
void string_array_reset(string_array_t* sa)
{
for ( size_t i = 0; i < sa->length; i++ )
free(sa->strings[i]);
free(sa->strings);
*sa = string_array_make();
}
size_t string_array_find(string_array_t* sa, const char* str)
{
for ( size_t i = 0; i < sa->length; i++ )
if ( !strcmp(sa->strings[i], str) )
return i;
return SIZE_MAX;
}
bool string_array_contains(string_array_t* sa, const char* str)
{
return string_array_find(sa, str) != SIZE_MAX;
}
bool string_array_append(string_array_t* sa, const char* str)
{
if ( sa->length == sa->capacity )
{
size_t new_capacity = sa->capacity ? sa->capacity * 2 : 8;
size_t new_size = sizeof(char*) * new_capacity;
char** new_strings = (char**) realloc(sa->strings, new_size);
if ( !new_strings )
return false;
sa->strings = new_strings;
sa->capacity = new_capacity;
}
char* copy = str ? strdup(str) : NULL;
if ( str && !copy )
return false;
sa->strings[sa->length++] = copy;
return true;
}
int child()
{
pid_t init_pid = getppid();
char init_pid_str[sizeof(pid_t)*3];
snprintf(init_pid_str, sizeof(pid_t)*3, "%ju", (uintmax_t) init_pid);
setenv("INIT_PID", init_pid_str, 1);
setpgid(0, 0);
tcsetpgrp(0, getpid());
const char* default_shell = "sh";
const char* default_home = "/root";
const char* shell;
const char* home;
if ( struct passwd* passwd = getpwuid(getuid()) )
{
setenv("USERNAME", passwd->pw_name, 1);
home = passwd->pw_dir[0] ? passwd->pw_dir : default_home;
setenv("HOME", home, 1);
shell = passwd->pw_shell[0] ? passwd->pw_shell : default_shell;
setenv("SHELL", shell, 1);
}
else
{
setenv("USERNAME", "root", 1);
setenv("HOME", home = default_home, 1);
setenv("SHELL", shell = default_shell, 1);
}
chdir(home);
const char* newargv[] = { shell, NULL };
execvp(shell, (char* const*) newargv);
error(0, errno, "%s", shell);
return 2;
}
int runsystem()
{
pid_t childpid = fork();
if ( childpid < 0 )
error(2, errno, "fork");
if ( childpid )
{
int status;
waitpid(childpid, &status, 0);
// TODO: Use the proper macro!
if ( 128 <= WEXITSTATUS(status) || WIFSIGNALED(status) )
{
printf("Looks like the system crashed, trying to bring it back up.\n");
return runsystem();
}
return WEXITSTATUS(status);
}
exit(child());
}
int chain_boot_path(const char* path)
{
// Run the next init program and restart it in case of a crash.
try_reboot_system:
if ( pid_t child_pid = fork() )
{
int status;
waitpid(child_pid, &status, 0);
// TODO: Use the proper macro!
if ( 128 <= WEXITSTATUS(status) || WIFSIGNALED(status) )
{
printf("Looks like the system crashed, trying to bring it back up.\n");
goto try_reboot_system;
}
return WEXITSTATUS(status);
}
// Switch to the new root directory,
chroot(path);
chdir("/");
const char* init_path = "/bin/init";
execl(init_path, init_path, NULL);
exit(127);
}
int init_emergency(int errnum, const char* format, ...)
{
fprintf(stderr, "init: emergency: ");
va_list ap;
va_start(ap, format);
vfprintf(stderr, format, ap);
va_end(ap);
if ( errnum )
fprintf(stderr, ": %s", strerror(errnum));
fprintf(stderr, "\n");
fprintf(stderr, "init: Dropping you to an emergency shell.\n");
fprintf(stderr, "init: Run `init' again when you have resolved the "
"situation to continue.\n");
return runsystem();
}
void add_block_devices_to_string_array(const char* path, string_array_t* sa)
{
DIR* dir = opendir(path);
if ( !dir )
return;
while ( struct dirent* entry = readdir(dir) )
{
if ( entry->d_name[0] == '.' )
continue;
char* dev_path = join_paths(path, entry->d_name);
struct stat st;
if ( !(stat(dev_path, &st) == 0 &&
S_ISBLK(st.st_mode) &&
string_array_append(sa, dev_path)) )
free(dev_path);
}
closedir(dir);
}
bool is_ext2_filesystem(const char* path, const char* uuid = NULL)
{
if ( pid_t child_pid = fork() )
{
int exit_status;
waitpid(child_pid, &exit_status, 0);
return WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0;
}
if ( uuid )
execlp("extfs", "extfs", "--probe", "--test-uuid", uuid, path, NULL);
else
execlp("extfs", "extfs", "--probe", path, NULL);
exit(127);
}
bool is_master_boot_record(const char* path)
{
if ( pid_t child_pid = fork() )
{
int exit_status;
waitpid(child_pid, &exit_status, 0);
return WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0;
}
execlp("mbrfs", "mbrfs", "--probe", path, NULL);
exit(127);
}
bool create_master_boot_record_partitions(const char* path, string_array_t* sa)
{
int pipe_fds[2];
pipe(pipe_fds);
if ( pid_t child_pid = fork() )
{
close(pipe_fds[1]);
FILE* mbrfp = fdopen(pipe_fds[0], "r");
while ( char* partition = read_single_line(mbrfp) )
{
if ( string_array_contains(sa, partition) ||
!string_array_append(sa, partition) )
free(partition);
}
fclose(mbrfp);
int exit_status;
waitpid(child_pid, &exit_status, 0);
return WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0;
}
dup2(pipe_fds[1], 1);
close(pipe_fds[0]);
close(pipe_fds[1]);
execlp("mbrfs", "mbrfs", path, NULL);
exit(127);
}
int chain_boot_device(const char* dev_path)
{
// Create a directory where we will mount the root filesystem.
const char* mount_point = "/fs";
const char* mount_point_dev = "/fs/dev";
mkdir(mount_point, 0666);
// Get information about the mount point before mounting.
struct stat orig_st, new_st;
stat(mount_point, &orig_st);
// Spawn the filesystem server for the root filesystem.
pid_t fs_pid = fork();
if ( !fs_pid )
{
execlp("extfs", "extfs", "--foreground", dev_path, mount_point, NULL);
exit(127);
}
// Wait for the filesystem server to come online.
struct timespec mount_wait_ts = timespec_make(0, 50L * 1000L * 1000L);
do nanosleep(&mount_wait_ts, NULL), stat(mount_point, &new_st);
while ( new_st.st_ino == orig_st.st_ino && new_st.st_dev == orig_st.st_dev );
// Create a device directory in the root filesystem.
mkdir(mount_point_dev, 0666);
// Mount the current device directory inside the new root filesystem.
int old_dev_fd = open("/dev", O_DIRECTORY | O_RDONLY);
int new_dev_fd = open(mount_point_dev, O_DIRECTORY | O_RDONLY);
fsm_fsbind(old_dev_fd, new_dev_fd, 0);
close(new_dev_fd);
close(old_dev_fd);
int ret = chain_boot_path(mount_point);
int root_fd = open(mount_point, O_RDONLY);
if ( 0 <= root_fd )
{
fsync(root_fd);
close(root_fd);
}
unmount(mount_point, 0);
int fs_exitstatus;
waitpid(fs_pid, &fs_exitstatus, 0);
if ( ret == 127 )
return init_emergency(errno, "Unable to locate the next init program");
return ret;
}
int chain_boot_uuid(const char* root_uuid)
{
string_array_t block_devices = string_array_make();
add_block_devices_to_string_array("/dev", &block_devices);
string_array_t root_block_devices = string_array_make();
// Scan through all the block devices and check for a filesystem with the
// desired uuid while creating partitions if encountering partition tables.
for ( size_t i = 0; i < block_devices.length; i++ )
{
const char* device_path = block_devices.strings[i];
assert(device_path);
if ( is_ext2_filesystem(device_path) )
{
if ( is_ext2_filesystem(device_path, root_uuid) )
string_array_append(&root_block_devices, device_path);
}
else if ( is_master_boot_record(device_path) )
{
create_master_boot_record_partitions(device_path, &block_devices);
}
}
string_array_reset(&block_devices);
// Panic if we are unable to locate the desired root filesystem.
if ( !root_block_devices.length )
return init_emergency(0, "Unable to locate root filesystem with uuid="
"`%s'", root_uuid);
// If we only found a single matching filesystem, we can just boot it.
if ( root_block_devices.length == 1 )
return chain_boot_device(root_block_devices.strings[0]);
// Handle the case where multiple root filesystems with the correct uuid is
// found - we have to ask the user for help in this case.
fprintf(stderr, "init: Found multiple devices with uuid=`%s'.\n", root_uuid);
fprintf(stderr, "init: Select the correct boot device or nothing to get an emergency shell.\n");
retry_ask_root_block_device:
for ( size_t i = 0; i < root_block_devices.length; i++ )
fprintf(stderr, "%zu.\t%s\n", i, root_block_devices.strings[i]);
printf("Enter index or name of boot device [root shell]: ");
fflush(stdout);
char* input = read_single_line(stdin);
if ( !input )
return init_emergency(errno, "Unable read line from standard input");
if ( !input[0] )
return init_emergency(0, "ambigious root filesystem - shell selected");
char* input_end;
unsigned long index = strtoul(input, &input_end, 0);
if ( *input_end )
{
if ( string_array_contains(&root_block_devices, input) )
return chain_boot_device(input);
fprintf(stderr, "init: error: `%s' is not an allowed choice\n", input);
goto retry_ask_root_block_device;
}
if ( root_block_devices.length <= index )
{
fprintf(stderr, "init: error: `%lu' is not an allowed choice\n", index);
goto retry_ask_root_block_device;
}
return chain_boot_device(root_block_devices.strings[index]);
}
void set_hostname()
{
FILE* hostname_fp = fopen("/etc/hostname", "r");
if ( !hostname_fp )
{
if ( errno == ENOENT )
return;
error(0, errno, "unable to open /etc/hostname, hostname is not set");
return;
}
char* hostname = read_single_line(hostname_fp);
if ( !hostname )
{
error(0, errno, "unable to read /etc/hostname, hostname is not set");
fclose(hostname_fp);
return;
}
fclose(hostname_fp);
if ( sethostname(hostname, strlen(hostname) + 1) < 0 )
{
error(0, errno, "unable to set hostname to `%s'", hostname);
free(hostname);
return;
}
free(hostname);
}
int init_main(int argc, char* argv[])
{
if ( 3 <= argc && !strcmp(argv[1], "--chain") )
return chain_boot_device(argv[2]);
// Reset the terminal's color and the rest of it.
printf(BRAND_INIT_BOOT_MESSAGE);
fflush(stdout);
// Set the default file creation mask.
umask(022);
// Set up the PATH variable.
setenv("PATH", "/bin", 1);
// Set the terminal type.
setenv("TERM", "sortix", 1);
// Make sure that we have a /tmp directory.
mkdir("/tmp", 01777);
// Set the hostname as found in /etc/hostname.
set_hostname();
// Find the uuid of the root filesystem.
const char* root_uuid_file = "/etc/init/rootfs.uuid";
FILE* root_uuid_fp = fopen(root_uuid_file, "r");
// If there is no uuid of the root filesystem, the current root filesystem
// is the real and final root filesystem and we boot it.
if ( !root_uuid_fp && errno == ENOENT )
return runsystem();
if ( !root_uuid_fp )
init_emergency(errno, "unable to open: `%s'", root_uuid_file);
char* root_uuid = read_single_line(root_uuid_fp);
if ( !root_uuid )
init_emergency(errno, "unable to read: `%s'", root_uuid_file);
fclose(root_uuid_fp);
return chain_boot_uuid(root_uuid);
}
int main(int argc, char* argv[])
{
if ( getpid() == 1 )
{
pid_t direct_child_pid = fork();
if ( direct_child_pid < 0 )
error(2, errno, "fork");
if ( direct_child_pid )
{
int status;
while ( true )
{
pid_t child_pid = waitpid(-1, &status, 0);
if ( child_pid < 0 )
error(2, errno, "waitpid");
if ( child_pid == direct_child_pid )
break;
}
int exit_value = WEXITSTATUS(status);
// TODO: Broadcast SIGKILL and wait for all processes to finish.
while ( 0 < waitpid(-1, &status, WNOHANG) )
{
}
return exit_value;
}
}
return init_main(argc, argv);
}