/* * Copyright (c) 2015, 2016, 2020, 2021, 2022, 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. * * sysinstall.c * Operating system installer. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Sortix libc doesn't have its own proper at this time. #if defined(__sortix__) #include #endif #include #include #include #include #include #include #include "autoconf.h" #include "conf.h" #include "devices.h" #include "execute.h" #include "fileops.h" #include "interactive.h" #include "manifest.h" #include "release.h" const char* prompt_man_section = "7"; const char* prompt_man_page = "installation"; static struct partition_table* search_bios_boot_pt(struct filesystem* root_fs) { char firmware[64]; if ( kernelinfo("firmware", firmware, sizeof(firmware)) != 0 ) return NULL; if ( strcmp(firmware, "bios") != 0 ) return NULL; struct blockdevice* bdev = root_fs->bdev; while ( bdev->p ) bdev = bdev->p->parent_bdev; if ( !bdev->pt ) return NULL; struct partition_table* pt = bdev->pt; if ( pt->type != PARTITION_TABLE_TYPE_GPT ) return NULL; return pt; } static struct partition* search_bios_boot_search(struct partition_table* pt) { for ( size_t i = 0; i < pt->partitions_count; i++ ) { struct partition* p = pt->partitions[i]; if ( p->bdev.fs && !strcmp(p->bdev.fs->fstype_name, "biosboot") ) return p; } return NULL; } static struct partition* search_bios_boot_partition(struct filesystem* root_fs) { struct partition_table* pt = search_bios_boot_pt(root_fs); if ( !pt ) return NULL; return search_bios_boot_search(pt); } static bool missing_bios_boot_partition(struct filesystem* root_fs) { struct partition_table* pt = search_bios_boot_pt(root_fs); if ( !pt ) return NULL; return !search_bios_boot_search(pt); } static bool should_install_bootloader_path(const char* mnt, struct blockdevice* bdev) { char* release_errpath; if ( asprintf(&release_errpath, "%s: /etc/sortix-release", path_of_blockdevice(bdev)) < 0 ) { warn("malloc"); return false; } char* release_path; if ( asprintf(&release_path, "%s/etc/sortix-release", mnt) < 0 ) { warn("malloc"); free(release_errpath); return false; } struct release release; if ( !os_release_load(&release, release_path, release_errpath) ) { free(release_path); free(release_errpath); return false; } free(release_path); free(release_errpath); release_free(&release); char* conf_path; if ( asprintf(&conf_path, "%s/etc/upgrade.conf", mnt) < 0 ) { warn("malloc"); return false; } struct conf conf; conf_init(&conf); bool result = false; if ( conf_load(&conf, conf_path) ) result = conf.grub; else if ( errno != ENOENT ) warn("%s: /etc/upgrade.conf", path_of_blockdevice(bdev)); conf_free(&conf); free(conf_path); return result; } static bool should_install_bootloader_bdev(struct blockdevice* bdev) { if ( !bdev->fs ) return false; if ( bdev->fs->flags & FILESYSTEM_FLAG_NOT_FILESYSTEM ) return false; if ( !bdev->fs->driver ) return false; char mnt[] = "/tmp/fs.XXXXXX"; if ( !mkdtemp(mnt) ) { warn("mkdtemp: %s", "/tmp/fs.XXXXXX"); return false; } struct mountpoint mp = { 0 }; mp.absolute = mnt; mp.fs = bdev->fs; mp.entry.fs_file = mnt; if ( !mountpoint_mount(&mp) ) { rmdir(mnt); return false; } bool should = should_install_bootloader_path(mnt, bdev); mountpoint_unmount(&mp); rmdir(mnt); return should; } static bool should_install_bootloader(void) { bool any_systems = false; for ( size_t i = 0; i < hds_count; i++ ) { struct harddisk* hd = hds[i]; if ( hd->bdev.pt ) { for ( size_t n = 0; n < hd->bdev.pt->partitions_count; n++ ) { any_systems = true; struct partition* p = hd->bdev.pt->partitions[n]; if ( should_install_bootloader_bdev(&p->bdev) ) return true; } } else if ( hd->bdev.fs ) { any_systems = true; if ( should_install_bootloader_bdev(&hd->bdev) ) return true; } } return !any_systems; } static bool passwd_check(const char* passwd_path, bool (*check)(struct passwd*, void*), void* check_ctx) { FILE* passwd = fopen(passwd_path, "r"); if ( !passwd ) { if ( errno != ENOENT ) warn("%s", passwd_path); return false; } char* line = NULL; size_t size = 0; ssize_t length; while ( 0 < (length = getline(&line, &size, passwd) ) ) { if ( line[size - 1] == '\n' ) line[--size] = '\0'; struct passwd pwd; if ( scanpwent(line, &pwd) && check(&pwd, check_ctx) ) { free(line); fclose(passwd); return true; } } free(line); if ( ferror(passwd) ) warn("%s", passwd_path); fclose(passwd); return false; } static bool passwd_has_uid_check(struct passwd* pwd, void* ctx) { return pwd->pw_uid == *(uid_t*) ctx; } static bool passwd_has_uid(const char* passwd_path, uid_t uid) { return passwd_check(passwd_path, passwd_has_uid_check, &uid); } static bool passwd_has_name_check(struct passwd* pwd, void* ctx) { return !strcmp(pwd->pw_name, (const char*) ctx); } static bool passwd_has_name(const char* passwd_path, const char* name) { return passwd_check(passwd_path, passwd_has_name_check, (void*) name); } static void install_skel(const char* home, uid_t uid, gid_t gid) { const char* argv[] = { "cp", "-RT", "--", "etc/skel", home, NULL }; execute(argv, "ug", uid, gid); } __attribute__((format(printf, 3, 4))) static bool install_configurationf(const char* path, const char* mode, const char* format, ...) { FILE* fp = fopen(path, mode); if ( !fp ) { warn("%s", path); return false; } va_list ap; va_start(ap, format); int status = vfprintf(fp, format, ap); va_end(ap); if ( status < 0 ) { warn("%s", path); fclose(fp); return false; } if ( fclose(fp) == EOF ) { warn("%s", path); return false; } return true; } static void grub_hash_password(char* buffer, size_t buffer_size, const char* pw) { int pipe_fds[2]; if ( pipe(pipe_fds) < 0 ) err(2, "pipe"); pid_t pid = fork(); if ( pid < 0 ) err(2, "fork"); if ( pid == 0 ) { close(pipe_fds[0]); if ( dup2(pipe_fds[1], 1) < 0 ) _exit(2); close(pipe_fds[1]); const char* argv[] = { "grub-mkpasswd-pbkdf2", "-p", pw, NULL }; execvp(argv[0], (char* const*) argv); _exit(127); } close(pipe_fds[1]); size_t done = 0; while ( done < buffer_size ) { ssize_t amount = read(pipe_fds[0], buffer + done, buffer_size - done); if ( amount < 0 ) err(2, "read"); if ( amount == 0 ) break; done += amount; } if ( done && buffer[done-1] == '\n' ) done--; if ( done == buffer_size ) done--; buffer[done] = '\0'; close(pipe_fds[0]); int exit_code; waitpid(pid, &exit_code, 0); if ( !WIFEXITED(exit_code) || WEXITSTATUS(exit_code) != 0 ) errx(2, "grub password hash failed"); } static const char* const ignore_kernel_options[] = { "--no-random-seed", "--random-seed", NULL, }; static char* normalize_kernel_options(void) { char* options = akernelinfo("options"); if ( !options ) { warn("kernelinfo: options"); return NULL; } size_t i = 0, o = 0; while ( options[i] ) { if ( isspace((unsigned char) options[i]) ) { i++; continue; } if ( options[i] != '-' ) // Imperfect since quoting options is allowed. break; if ( !strncmp(options + i, "--", 2) && (!options[i + 2] || isspace((unsigned char) options[i + 2])) ) break; bool ignored = false; for ( size_t n = 0; ignore_kernel_options[n]; n++ ) { const char* opt = ignore_kernel_options[n]; size_t len = strlen(opt); if ( !strncmp(options + i, opt, len) && (!options[i + len] || isspace((unsigned char) options[i + len])) ) { i += len; ignored = true; break; } } if ( ignored ) continue; bool singly = false; bool doubly = false; bool escaped = false; for ( ; options[i]; i++ ) { char c = options[i]; options[o++] = c; 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; } escaped = false; } } while ( o && isspace((unsigned char) options[o - 1]) ) o--; options[o] = '\0'; return options; } static pid_t main_pid; static struct mountpoint* mountpoints; static size_t mountpoints_used; static bool etc_made = false; static char etc[] = "/tmp/etc.XXXXXX"; static bool fs_made = false; static char fs[] = "/tmp/fs.XXXXXX"; static int exit_gui_code = -1; static void unmount_all_but_root(void) { for ( size_t n = mountpoints_used; n != 0; n-- ) { size_t i = n - 1; struct mountpoint* mountpoint = &mountpoints[i]; if ( !strcmp(mountpoint->entry.fs_file, "/") ) continue; mountpoint_unmount(mountpoint); } } void exit_handler(void) { if ( getpid() != main_pid ) return; chdir("/"); for ( size_t n = mountpoints_used; n != 0; n-- ) { size_t i = n - 1; struct mountpoint* mountpoint = &mountpoints[i]; mountpoint_unmount(mountpoint); } if ( fs_made ) rmdir(fs); if ( etc_made ) execute((const char*[]) { "rm", "-rf", etc, NULL }, ""); if ( 0 <= exit_gui_code ) gui_shutdown(exit_gui_code); } void exit_gui(int code) { exit_gui_code = code; exit(code); } static void cancel_on_sigint(int signum) { (void) signum; errx(2, "fatal: Installation canceled"); } int main(void) { shlvl(); if ( !isatty(0) ) errx(2, "fatal: stdin is not a terminal"); if ( !isatty(1) ) errx(2, "fatal: stdout is not a terminal"); if ( !isatty(2) ) errx(2, "fatal: stderr is not a terminal"); if ( getuid() != 0 ) errx(2, "You need to be root to install %s", BRAND_DISTRIBUTION_NAME); if ( getgid() != 0 ) errx(2, "You need to be group root to install %s", BRAND_DISTRIBUTION_NAME); main_pid = getpid(); if ( atexit(exit_handler) != 0 ) err(2, "atexit"); if ( !mkdtemp(etc) ) err(2, "mkdtemp: %s", "/tmp/etc.XXXXXX"); etc_made = true; // Export for the convenience of users escaping to a shell. setenv("SYSINSTALL_ETC", fs, 1); if ( chdir(etc) < 0 ) err(2, "chdir: %s", etc); struct utsname uts; uname(&uts); struct conf conf; conf_init(&conf); if ( !conf_load(&conf, "/etc/upgrade.conf") && errno != ENOENT ) warn("/etc/upgrade.conf"); autoconf_load("/etc/autoinstall.conf"); char* accepts_defaults = autoconf_eval("accept_defaults"); bool non_interactive = accepts_defaults && !strcasecmp(accepts_defaults, "yes"); free(accepts_defaults); static char input[256]; textf("Hello and welcome to the " BRAND_DISTRIBUTION_NAME " " VERSIONSTR "" " installer for %s.\n\n", uts.machine); if ( non_interactive || (autoconf_has("ready") && (autoconf_has("disked") || autoconf_has("confirm_install"))) ) { int countdown = 10; if ( autoconf_has("countdown") ) { char* string = autoconf_eval("countdown"); countdown = atoi(string); free(string); } sigset_t old_set; sigset_t new_set; sigemptyset(&new_set); sigaddset(&new_set, SIGINT); sigprocmask(SIG_BLOCK, &new_set, &old_set); struct sigaction old_sa; struct sigaction new_sa = { 0 }; new_sa.sa_handler = cancel_on_sigint; sigaction(SIGINT, &new_sa, &old_sa); for ( ; 0 < countdown; countdown-- ) { textf("Automatically installing " BRAND_DISTRIBUTION_NAME " " VERSIONSTR " in %i %s... (Control-C to cancel)\n", countdown, countdown != 1 ? "seconds" : "second"); sigprocmask(SIG_SETMASK, &old_set, NULL); sleep(1); sigprocmask(SIG_BLOCK, &new_set, &old_set); } textf("Automatically installing " BRAND_DISTRIBUTION_NAME " " VERSIONSTR "...\n"); text("\n"); sigaction(SIGINT, &old_sa, NULL); sigprocmask(SIG_SETMASK, &old_set, NULL); } // '|' rather than '||' is to ensure side effects. if ( missing_program("cut") | missing_program("dash") | missing_program("fsck.ext2") | missing_program("grub-install") | missing_program("man") | missing_program("sed") | missing_program("xargs") ) { text("Warning: This system does not have the necessary third party " "software installed to properly install this operating system.\n"); while ( true ) { prompt(input, sizeof(input), "ignore_missing_programs", "Sure you want to proceed?", "no"); if ( strcasecmp(input, "no") == 0 ) return 0; if ( strcasecmp(input, "yes") == 0 ) break; } text("\n"); } text("You are about to install a new operating system on this computer. " "This is not something you should do on a whim or when you are " "impatient. Take the time to read the documentation and be patient " "while you learn the new system. This is a very good time to start an " "external music player that plays soothing classical music on loop.\n\n"); if ( !access_or_die("/tix/tixinfo/ssh", F_OK) && access_or_die("/root/.ssh/authorized_keys", F_OK) < 0 ) text("If you wish to ssh into your new installation, it's recommended " "to first add your public keys to the .iso and obtain " "fingerprints per release-iso-modification(7) before installing." "\n\n"); const char* readies[] = { "Ready", "Yes", "Yeah", "Yep", "Let's go", "Let's do this", "Betcha", "Sure am", "You bet", "It's very good music", }; size_t num_readies = sizeof(readies) / sizeof(readies[0]); const char* ready = readies[arc4random_uniform(num_readies)]; if ( autoconf_has("disked") ) text("Warning: This installer will perform automatic harddisk " "partitioning!\n"); if ( autoconf_has("confirm_install") ) text("Warning: This installer will automatically install an operating " "system!\n"); prompt(input, sizeof(input), "ready", "Ready?", ready); text("\n"); text("This is not yet a fully fledged operating system. You should adjust " "your expectations accordingly. You should not consider the system safe " "for multi-user use. Filesystem permissions are not enforced yet. There " "are many security issues so setuid(2) blatantly allows every user to " "become root if they want to.\n\n"); text("You can always escape to a shell by answering '!' to any regular " "prompt. You can view the installation(7) manual page by " "answering '!man'. Default answers are in []'s and can be selected by " "pressing enter.\n\n"); // TODO: You can leave this program by pressing ^C but it can leave your // system in an inconsistent state. install_configurationf("upgrade.conf", "a", "src = yes\n"); bool kblayout_setable = 0 <= tcgetblob(0, "kblayout", NULL, 0) || getenv("DISPLAY_SOCKET"); while ( kblayout_setable ) { // TODO: Detect the name of the current keyboard layout. prompt(input, sizeof(input), "kblayout", "Choose your keyboard layout ('?' or 'L' for list)", "default"); if ( !strcmp(input, "?") || !strcmp(input, "l") || !strcmp(input, "L") ) { DIR* dir = opendir("/share/kblayout"); if ( !dir ) { warn("%s", "/share/kblayout"); continue; } bool space = false; struct dirent* entry; while ( (entry = readdir(dir)) ) { if ( entry->d_name[0] == '.' ) continue; if ( space ) putchar(' '); fputs(entry->d_name, stdout); space = true; } closedir(dir); if ( !space ) fputs("(No keyboard layouts available)", stdout); putchar('\n'); continue; } if ( !strcmp(input, "default") ) break; const char* argv[] = { "chkblayout", "--", input, NULL }; if ( execute(argv, "f") == 0 ) break; } if ( kblayout_setable ) { if ( !input[0] || !strcmp(input, "default") ) text("/etc/kblayout will not be created (default).\n"); else { textf("/etc/kblayout will be set to \"%s\".\n", input); mode_t old_umask = getumask(); umask(022); install_configurationf("kblayout", "w", "%s\n", input); umask(old_umask); } text("\n"); } struct dispmsg_crtc_mode mode; if ( get_video_mode(&mode) ) { bool good = (mode.control & DISPMSG_CONTROL_VALID) && (mode.control & DISPMSG_CONTROL_GOOD_DEFAULT); if ( mode.control & DISPMSG_CONTROL_VM_AUTO_SCALE ) { text("The display resolution will automatically change to " "match the size of the virtual machine window.\n\n"); good = true; } const char* def = non_interactive || good ? "no" : "yes"; while ( true ) { prompt(input, sizeof(input), "videomode", "Select display resolution? " "(yes/no/WIDTHxHEIGHTxBPP)", def); unsigned int xres, yres, bpp; bool set = sscanf(input, "%ux%ux%u", &xres, &yres, &bpp) == 3; if ( !strcasecmp(input, "no") ) { input[0] = '\0'; break; } const char* r = set ? input : NULL; if ( execute((const char*[]) { "chvideomode", r, NULL }, "f") != 0 ) continue; input[0] = '\0'; if ( !get_video_mode(&mode) || !(mode.control & DISPMSG_CONTROL_VALID) || mode.control & DISPMSG_CONTROL_VGA ) continue; snprintf(input, sizeof(input), "%ux%ux%u", mode.view_xres, mode.view_yres, mode.fb_format); break; } if ( !input[0] ) text("/etc/videomode will not be created.\n"); else { textf("/etc/videomode will be set to \"%s\".\n", input); mode_t old_umask = getumask(); umask(022); install_configurationf("videomode", "w", "%s\n", input); umask(old_umask); } text("\n"); } text("Searching for existing installations...\n"); scan_devices(); bool bootloader_default = should_install_bootloader(); text("\n"); textf("You need a bootloader to start the operating system. GRUB is the " "standard %s bootloader and this installer comes with a copy. " "This copy is however unable to automatically detect other operating " "systems.\n\n", BRAND_DISTRIBUTION_NAME); text("Single-boot installations should accept this bootloader.\n"); text("Dual-boot systems should refuse it and manually arrange for " "bootloading by configuring any existing multiboot compliant " "bootloader.\n"); text("\n"); char accept_grub[10]; char accept_grub_password[10]; char grub_password[512]; while ( true ) { const char* def = bootloader_default ? "yes" : "no"; prompt(accept_grub, sizeof(accept_grub), "grub", "Install a new GRUB bootloader?", def); if ( strcasecmp(accept_grub, "no") == 0 || strcasecmp(accept_grub, "yes") == 0 ) break; } text("\n"); if ( strcasecmp(accept_grub, "yes") == 0 ) { install_configurationf("upgrade.conf", "a", "grub = yes\n"); text("If an unauthorized person has access to the bootloader command " "line, then the whole system security can be compromised. You can " "prevent this by password protecting interactive use of the " "bootloader, but still allowing anyone to start the system " "normally. Similarly you may wish to manually go into your " "firmware and password protect it.\n"); text("\n"); while ( true ) { const char* def = non_interactive && !autoconf_has("grub_password_hash") ? "no" : "yes"; prompt(accept_grub_password, sizeof(accept_grub_password), "grub_password", "Password protect interactive bootloader? (yes/no)", def); if ( strcasecmp(accept_grub_password, "no") == 0 || strcasecmp(accept_grub_password, "yes") == 0 ) break; } if ( autoconf_has("grub_password_hash") ) { char* hash = autoconf_eval("grub_password_hash"); install_configurationf("grubpw", "w", "%s\n", hash); free(hash); } else while ( !strcasecmp(accept_grub_password, "yes") ) { char first[128]; char second[128]; password(first, sizeof(first), "Bootloader root password? (will not echo)"); password(second, sizeof(second), "Bootloader root password? (again)"); if ( strcmp(first, second) != 0 ) { printf("Passwords do not match, try again.\n"); continue; } explicit_bzero(second, sizeof(second)); if ( !strcmp(first, "") ) { char answer[32]; prompt(answer, sizeof(answer), "grub_password_empty", "Empty password is stupid, are you sure? (yes/no)", "no"); if ( strcasecmp(answer, "yes") != 0 ) continue; } grub_hash_password(grub_password, sizeof(grub_password), first); textf("/etc/grubpw will be made with grub-mkpasswd-pbkdf2.\n"); mode_t old_umask = getumask(); umask(077); install_configurationf("grubpw", "w", "%s\n", grub_password); umask(old_umask); break; } text("\n"); } char* kernel_options = normalize_kernel_options(); if ( (autoconf_has("kernel_options") || (kernel_options && kernel_options[0])) && !access_or_die("/tix/tixinfo/grub", F_OK) ) { text("The operating system was booted with explicit kernel(7) options. " "Would you like set them permanently in /etc/grub?\n\n"); while ( true ) { char options[1024]; prompt(options, sizeof(options), "kernel_options", "Kernel options? (OPTIONS/no)", kernel_options); if ( !strcasecmp(options, "no") ) { kernel_options = NULL; break; } if ( options[0] ) { install_configurationf("grub", "w", "GRUB_CMDLINE_SORTIX='%s'\n", options); textf("/etc/grub will be made with the kernel options.\n"); } break; } text("\n"); } free(kernel_options); // TODO: Offer the user an automatic layout of partitions if the disk is // empty. // TODO: Perhaps let the user know the size of the system that will be // installed? text("You need to select a root filesystem and other mountpoints now. You " "will now be dumped into a partition editor. Create and format a " "root filesystem partition as needed.\n"); text("\n"); const char* mktable_tip = ""; if ( check_lacking_partition_table() ) mktable_tip = "Type mktable to make a new partition table. "; const char* devices_tip = ""; if ( check_multiple_harddisks() ) devices_tip = "Type devices to list the devices. " "Type device 1 to switch to device 1. "; textf("Type ls to list partitions on the device. " "%s" "%s" "Type mkpart to make a new partition. " "Type mount 2 / to create a mountpoint for partition 2. " "Type exit when done. " "There is partitioning advice in installation(7). " "Type man 8 disked to display the disked(8) man page.\n", mktable_tip, devices_tip); struct filesystem* root_filesystem = NULL; struct filesystem* boot_filesystem = NULL; struct filesystem* bootloader_filesystem = NULL; bool not_first = false; while ( true ) { if ( not_first ) text("Type man to display the disked(8) man page.\n"); not_first = true; const char* argv[] = { "disked", "--fstab=fstab", NULL }; char* disked_input = autoconf_eval("disked"); if ( execute(argv, "fi", disked_input) != 0 ) { free(disked_input); // TODO: We also end up here on SIGINT. // TODO: Offer a shell here instead of failing? warnx("partitioning failed"); sleep(1); continue; } free(disked_input); free_mountpoints(mountpoints, mountpoints_used); mountpoints = NULL; mountpoints_used = 0; scan_devices(); if ( !load_mountpoints("fstab", &mountpoints, &mountpoints_used) ) { if ( errno == ENOENT ) text("You have not created any mountpoints. Try again.\n"); else warn("fstab"); continue; } bool found_rootfs = false; for ( size_t i = 0; !found_rootfs && i < mountpoints_used; i++ ) if ( !strcmp(mountpoints[i].entry.fs_file, "/") ) found_rootfs = true; if ( !found_rootfs ) { text("You have no root filesystem mountpoint. Try again.\n"); continue; } root_filesystem = NULL; boot_filesystem = NULL; bool cant_mount = false; for ( size_t i = 0; i < mountpoints_used; i++ ) { struct mountpoint* mnt = &mountpoints[i]; const char* spec = mnt->entry.fs_spec; if ( !(mnt->fs = search_for_filesystem_by_spec(spec)) ) { warnx("fstab: %s: Found no mountable filesystem matching `%s'", mnt->entry.fs_file, spec); cant_mount = true; continue; } if ( !mnt->fs->driver ) { warnx("fstab: %s: %s: Don't know how to mount this %s filesystem", mnt->entry.fs_file, path_of_blockdevice(mnt->fs->bdev), mnt->fs->fstype_name); cant_mount = true; continue; } if ( !strcmp(mnt->entry.fs_file, "/") ) root_filesystem = mnt->fs; if ( !strcmp(mnt->entry.fs_file, "/boot") ) boot_filesystem = mnt->fs; } if ( cant_mount ) continue; assert(root_filesystem); bootloader_filesystem = boot_filesystem ? boot_filesystem : root_filesystem; assert(bootloader_filesystem); if ( !strcasecmp(accept_grub, "yes") && missing_bios_boot_partition(bootloader_filesystem) ) { const char* where = boot_filesystem ? "/boot" : "root"; const char* dev = device_path_of_blockdevice(bootloader_filesystem->bdev); assert(dev); textf("You are a installing BIOS bootloader and the %s " "filesystem is located on a GPT partition, but you haven't " "made a BIOS boot partition on the %s GPT disk. Pick " "biosboot during mkpart and make a 1 MiB partition.\n", where, dev); char return_to_disked[10]; while ( true ) { prompt(return_to_disked, sizeof(return_to_disked), "missing_bios_boot_partition", "Return to disked to make a BIOS boot partition?", "yes"); if ( strcasecmp(accept_grub, "no") == 0 || strcasecmp(accept_grub, "yes") == 0 ) break; } if ( !strcasecmp(return_to_disked, "yes") ) continue; text("Proceeding, but expect the installation to fail.\n"); } break; } text("\n"); textf("We are now ready to install %s %s. Take a moment to verify " "everything is in order.\n", BRAND_DISTRIBUTION_NAME, VERSIONSTR); text("\n"); printf(" %-16s system architecture\n", uts.machine); for ( size_t i = 0; i < mountpoints_used; i++ ) { struct mountpoint* mnt = &mountpoints[i]; const char* devname = path_of_blockdevice(mnt->fs->bdev); const char* where = mnt->entry.fs_file; printf(" %-16s use as %s\n", devname, where); } if ( strcasecmp(accept_grub, "yes") == 0 ) { struct partition* bbp = search_bios_boot_partition(bootloader_filesystem); if ( bbp ) printf(" %-16s bios boot partition\n", path_of_blockdevice(&bbp->bdev)); printf(" %-16s bootloader installation target\n", device_path_of_blockdevice(bootloader_filesystem->bdev)); } text("\n"); while ( true ) { prompt(input, sizeof(input), "confirm_install", "Install " BRAND_DISTRIBUTION_NAME "? " "(yes/no/exit/poweroff/reboot/halt)", "yes"); if ( !strcasecmp(input, "yes") ) break; else if ( !strcasecmp(input, "no") ) { text("Answer '!' to get a shell. Type !man to view the " "installation(7) manual page.\n"); text("Alternatively, you can answer 'poweroff', 'reboot', or " "'halt' to cancel the installation.\n"); continue; } else if ( !strcasecmp(input, "exit") ) exit(0); else if ( !strcasecmp(input, "poweroff") ) exit_gui(0); else if ( !strcasecmp(input, "reboot") ) exit_gui(1); else if ( !strcasecmp(input, "halt") ) exit_gui(2); else continue; } text("\n"); text("Installing " BRAND_DISTRIBUTION_NAME " " VERSIONSTR " now:\n"); printf(" - Mounting filesystems...\n"); if ( !mkdtemp(fs) ) err(2, "mkdtemp: %s", "/tmp/fs.XXXXXX"); fs_made = true; // Export for the convenience of users escaping to a shell. setenv("SYSINSTALL_TARGET", fs, 1); for ( size_t i = 0; i < mountpoints_used; i++ ) { struct mountpoint* mnt = &mountpoints[i]; char* absolute; if ( asprintf(&absolute, "%s%s", fs, mnt->absolute) < 0 ) err(2, "asprintf"); free(mnt->absolute); mnt->absolute = absolute; if ( mkdir_p(mnt->absolute, 0755) < 0 ) err(2, "mkdir: %s", mnt->absolute); if ( !mountpoint_mount(mnt) ) exit(2); } if ( chdir(fs) < 0 ) err(2, "chdir: %s", fs); pid_t install_pid = fork(); if ( install_pid < 0 ) err(2, "fork"); if ( install_pid == 0 ) { printf(" - Populating root filesystem...\n"); chmod(".", 0755); if ( access("tix/collection.conf", F_OK) < 0 ) execute((const char*[]) { "tix-collection", ".", "create", "--prefix=", NULL }, "_e"); install_manifests_detect("", ".", true, true, true); // TODO: Preserve the existing /src if it exists like in sysupgrade. if ( has_manifest("src") ) install_manifest("src", "", ".", (const char*[]){}, 0); printf(" - Creating configuration files...\n"); // TODO: Preserve mode/ownership/timestamps? execute((const char*[]) { "cp", "-RTP", etc, "etc", NULL }, "_e"); // TODO: Auto detect appropriate bcrypt rounds and set up etc/login.conf // and use those below instead of blowfish,a. if ( access_or_die("boot/random.seed", F_OK) < 0 ) { printf(" - Creating random seed...\n"); write_random_seed("boot/random.seed"); } printf(" - Creating initrd...\n"); execute((const char*[]) { "update-initrd", "--sysroot", fs, NULL }, "_e"); if ( strcasecmp(accept_grub, "yes") == 0 ) { printf(" - Installing bootloader...\n"); execute((const char*[]) { "chroot", "-d", ".", "grub-install", device_path_of_blockdevice(bootloader_filesystem->bdev), NULL }, "_eqQ"); printf(" - Configuring bootloader...\n"); execute((const char*[]) { "chroot", "-d", ".", "update-grub", NULL }, "_eqQ"); } else if ( access_or_die("/etc/default/grub.d/10_sortix", F_OK) == 0 ) { // Help dual booters by making /etc/grub.d/default/10_sortix.cache. printf(" - Creating bootloader fragment...\n"); execute((const char*[]) { "chroot", "-d", ".", "/etc/grub.d/default/10_sortix", NULL }, "_eq"); } printf(" - Finishing installation...\n"); _exit(0); } int install_code; waitpid(install_pid, &install_code, 0); if ( WIFEXITED(install_code) && WEXITSTATUS(install_code) == 0 ) { } else if ( WIFEXITED(install_code) ) errx(2, "installation failed with exit status %i", WEXITSTATUS(install_code)); else if ( WIFSIGNALED(install_code) ) errx(2, "installation failed: %s", strsignal(WTERMSIG(install_code))); else errx(2, "installation failed: unknown waitpid code %i", install_code); unsetenv("SYSINSTALL_ETC"); execute((const char*[]) { "rm", "-r", etc, NULL }, ""); etc_made = false; text("\n"); text("System files are now installed. We'll now make the system functional " "by configuring a few essential matters.\n\n"); umask(0022); if ( access("etc/hostname", F_OK) == 0 ) textf("/etc/hostname already exists, skipping creating it.\n"); else while ( true ) { char defhost[HOST_NAME_MAX + 1] = ""; if ( non_interactive ) gethostname(defhost, sizeof(defhost)); FILE* defhost_fp = fopen("etc/hostname", "r"); if ( defhost_fp ) { fgets(defhost, sizeof(defhost), defhost_fp); size_t defhostlen = strlen(defhost); if ( defhostlen && defhost[defhostlen-1] == '\n' ) defhost[defhostlen-1] = '\0'; fclose(defhost_fp); } char hostname[HOST_NAME_MAX + 1] = ""; prompt(hostname, sizeof(hostname), "hostname", "System hostname?", defhost[0] ? defhost : NULL); if ( !install_configurationf("etc/hostname", "w", "%s\n", hostname) ) continue; textf("/etc/hostname was set to \"%s\".\n", hostname); break; } text("\n"); if ( mkdir("root", 0700) < 0 ) { if ( errno == EEXIST ) { if ( chmod("root", 0700) < 0 ) warn("chmod: root"); } else warn("mkdir: root"); } if ( passwd_has_uid("etc/passwd", 0) || passwd_has_name("etc/passwd", "root") ) { textf("Root account already exists, skipping creating it.\n"); } else if ( non_interactive || autoconf_has("password_hash_root") ) { char* hash = autoconf_eval("password_hash_root"); if ( !hash && !(hash = strdup("x")) ) err(2, "malloc"); if ( !install_configurationf("etc/passwd", "a", "root:%s:0:0:root:/root:sh\n" "include /etc/default/passwd.d/*\n", hash) ) err(2, "etc/passwd"); textf("User '%s' added to /etc/passwd\n", "root"); if ( !install_configurationf("etc/group", "a", "root::0:root\n" "include /etc/default/group.d/*\n") ) err(2, "etc/passwd"); install_skel("/root", 0, 0); textf("Group '%s' added to /etc/group.\n", "root"); free(hash); } else while ( true ) { char first[128]; char second[128]; password(first, sizeof(first), "Password for root account? (will not echo)"); password(second, sizeof(second), "Password for root account? (again)"); if ( strcmp(first, second) != 0 ) { printf("Passwords do not match, try again.\n"); continue; } explicit_bzero(second, sizeof(second)); if ( !strcmp(first, "") ) { char answer[32]; prompt(answer, sizeof(answer), "empty_password", "Empty password is stupid, are you sure? (yes/no)", "no"); if ( strcasecmp(answer, "yes") != 0 ) continue; } char hash[128]; if ( crypt_newhash(first, "blowfish,a", hash, sizeof(hash)) < 0 ) { explicit_bzero(first, sizeof(first)); warn("crypt_newhash"); continue; } explicit_bzero(first, sizeof(first)); if ( !install_configurationf("etc/passwd", "a", "root:%s:0:0:root:/root:sh\n" "include /etc/default/passwd.d/*\n", hash) ) continue; textf("User '%s' added to /etc/passwd\n", "root"); if ( !install_configurationf("etc/group", "a", "root::0:root\n" "include /etc/default/group.d/*\n") ) continue; install_skel("/root", 0, 0); textf("Group '%s' added to /etc/group.\n", "root"); break; } struct ssh_file { const char* key; const char* path; const char* pub; }; const struct ssh_file ssh_files[] = { {"copy_ssh_authorized_keys_root", "/root/.ssh/authorized_keys", NULL}, {"copy_ssh_config_root", "/root/.ssh/config", NULL}, {"copy_ssh_id_rsa_root", "/root/.ssh/id_rsa", "/root/.ssh/id_rsa.pub"}, {"copy_ssh_known_hosts_root", "/root/.ssh/known_hosts", NULL}, }; size_t ssh_files_count = sizeof(ssh_files) / sizeof(ssh_files[0]); bool any_ssh_keys = false; for ( size_t i = 0; i < ssh_files_count; i++ ) { const struct ssh_file* file = &ssh_files[i]; if ( access_or_die(file->path, F_OK) < 0 ) continue; text("\n"); textf("Found %s\n", file->path); if ( file->pub && !access_or_die(file->pub, F_OK) ) textf("Found %s\n", file->pub); while ( true ) { char question[256]; snprintf(question, sizeof(question), "Copy %s from installer environment? (yes/no)", file->path); prompt(input, sizeof(input), file->key, question, "yes"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; mkdir_or_chmod_or_die("root/.ssh", 0700); textf("Copying %s -> %s\n", file->path, file->path + 1); execute((const char*[]) {"cp", file->path, file->path+ 1, NULL }, "f"); if ( file->pub ) { textf("Copying %s -> %s\n", file->pub, file->pub + 1); execute((const char*[]) {"cp", file->pub, file->pub + 1, NULL }, "f"); } any_ssh_keys = true; break; } } text("\n"); if ( mkdir("etc/init", 0755) < 0 ) { if ( errno == EEXIST ) { if ( chmod("etc/init", 0755) < 0 ) warn("chmod: etc/init"); } else warn("mkdir: etc/init"); } install_configurationf("etc/init/default", "w", "require multi-user exit-code\n"); text("Congratulations, the system is now functional! This is a good time " "to do further customization of the system.\n\n"); // TODO: autoconf users support. bool made_user = false; for ( uid_t uid = 1000; !has_autoconf; ) { while ( passwd_has_uid("etc/passwd", uid) ) uid++; gid_t gid = (gid_t) uid; static char userstr[256]; const char* question = "Setup a user? (enter username or 'no')"; if ( made_user ) question = "Setup another user? (enter username or 'no')"; prompt(userstr, sizeof(userstr), NULL, question, "no"); if ( !strcmp(userstr, "no") ) break; if ( !strcmp(userstr, "yes") ) continue; const char* user = userstr; while ( user[0] == ' ') user++; if ( passwd_has_name("etc/passwd", user) ) { textf("Account '%s' already exists.\n", user); continue; } static char name[256]; prompt(name, sizeof(name), NULL, "Full name of user?", user); char first[128]; char second[128]; while ( true ) { password(first, sizeof(first), "Password for user? (will not echo)"); password(second, sizeof(second), "Password for user? (again)"); if ( strcmp(first, second) != 0 ) { printf("Passwords do not match, try again.\n"); continue; } explicit_bzero(second, sizeof(second)); if ( !strcmp(first, "") ) { char answer[32]; prompt(answer, sizeof(answer), "empty_password", "Empty password is stupid, are you sure? (yes/no)", "no"); if ( strcasecmp(answer, "yes") != 0 ) continue; } break; } char hash[128]; if ( crypt_newhash(first, "blowfish,a", hash, sizeof(hash)) < 0 ) { explicit_bzero(first, sizeof(first)); warn("crypt_newhash"); continue; } explicit_bzero(first, sizeof(first)); if ( !install_configurationf("etc/passwd", "a", "%s:%s:%" PRIuUID ":%" PRIuGID ":%s:/home/%s:sh\n", user, hash, uid, gid, name, user) ) continue; if ( !install_configurationf("etc/group", "a", "%s::%" PRIuGID ":%s\n", user, gid, user) ) continue; char* home; if ( asprintf(&home, "home/%s", user) < 0 ) { warn("asprintf"); continue; } if ( mkdir(home, 0700) < 0 && errno != EEXIST ) { warn("mkdir: %s", home); free(home); continue; } chown(home, uid, gid); install_skel(home, uid, gid); free(home); textf("User '%s' added to /etc/passwd\n", user); textf("Group '%s' added to /etc/group.\n", user); text("\n"); uid++; made_user = true; } // TODO: autoconf support. if ( !has_autoconf ) text("\n"); // TODO: Ask if networking should be disabled / enabled. while ( true ) { prompt(input, sizeof(input), "enable_gui", "Enable graphical user interface?", getenv("DISPLAY_SOCKET") ? "yes" : "no"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; if ( !install_configurationf("etc/session", "w", "#!sh\nexec display\n") || chmod("etc/session", 0755) < 0 ) { warn("etc/session"); continue; } text("Added 'exec display' to /etc/session\n"); break; } text("\n"); if ( !access_or_die("/tix/tixinfo/ntpd", F_OK) ) { text("A Network Time Protocol client (ntpd) has been installed that " "can automatically synchronize the current time with the internet." "\n\n"); text("Privacy notice: If enabled, the default configuration will " "obtain time from pool.ntp.org and time.cloudflare.com; and " "compare with HTTPS timestamps from quad9 and www.google.com. " "You are encouraged to edit /etc/ntpd.conf per the ntpd.conf(5) " "manual with your preferences.\n\n"); bool copied = false; while ( true ) { prompt(input, sizeof(input), "enable_ntpd", "Automatically get time from the network? (yes/no/edit/man)", copied ? "yes" : "no"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "man") == 0 ) { execute((const char*[]) {"man", "5", "ntpd.conf", NULL}, "fi"); continue; } if ( strcasecmp(input, "edit") == 0 ) { if ( !copied ) { execute((const char*[]) {"cp", "etc/default/ntpd.conf", "etc/ntpd.conf", NULL}, "f"); copied = true; } const char* editor = getenv("EDITOR") ? getenv("EDITOR") : "editor"; execute((const char*[]) {editor, "etc/ntpd.conf", NULL}, "f"); text("Created /etc/ntpd.conf from /etc/default/ntpd.conf\n"); continue; } if ( strcasecmp(input, "yes") != 0 ) continue; if ( !install_configurationf("etc/init/local", "a", "require ntpd optional\n") ) { warn("etc/init/local"); continue; } if ( !install_configurationf("etc/init/time", "a", "furthermore\n" "require ntpd optional\n") ) { warn("etc/init/time"); continue; } text("Added 'require ntpd optional' to /etc/init/local\n"); text("Added 'require ntpd optional' to /etc/init/time\n"); break; } text("\n"); } struct sshd_key_file { const char* pri; const char* pub; }; const struct sshd_key_file sshd_key_files[] = { {"/etc/ssh_host_ecdsa_key", "/etc/ssh_host_ecdsa_key.pub"}, {"/etc/ssh_host_ed25519_key", "/etc/ssh_host_ed25519_key.pub"}, {"/etc/ssh_host_rsa_key", "/etc/ssh_host_rsa_key.pub"}, }; size_t sshd_key_files_count = sizeof(sshd_key_files) / sizeof(sshd_key_files[0]); bool any_sshd_keys = false; for ( size_t i = 0; i < sshd_key_files_count; i++ ) { if ( !access_or_die(sshd_key_files[i].pri, F_OK) ) { textf("Found %s\n", sshd_key_files[i].pri); any_sshd_keys = true; } } bool enabled_sshd = false; if ( !access_or_die("/tix/tixinfo/ssh", F_OK) ) { text("A ssh server has been installed. You have the option of starting " "it on boot to allow remote login over a cryptographically secure " "channel. Answer no if you don't know what ssh is.\n\n"); if ( !any_sshd_keys ) text("Warning: " BRAND_DISTRIBUTION_NAME " does not yet collect " "entropy for secure random numbers. Unless you type '!' and " "escape to a shell and put 256 bytes of actual randomness " "into boot/random.seed, the first boot will use the " "randomness of this installer environment to generate ssh " "keys. This initial randomness may be as weak as the wall " "time when you booted the installer, which is easily guessed " "by an attacker. The same warning applies to outgoing secure " "connections as well.\n\n"); bool might_want_sshd = any_ssh_keys || any_sshd_keys || !access_or_die("/etc/sshd_config", F_OK); while ( true ) { prompt(input, sizeof(input), "enable_sshd", "Enable ssh server? (yes/no)", might_want_sshd ? "yes" : "no"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; if ( !install_configurationf("etc/init/local", "a", "require sshd optional\n") ) { warn("etc/init/local"); continue; } enabled_sshd = true; text("Added 'require sshd optional' to /etc/init/local\n"); text("The ssh server will be started when the system boots.\n"); break; } text("\n"); } bool has_sshd_config = false; if ( !access_or_die("/etc/sshd_config", F_OK) ) { while ( true ) { prompt(input, sizeof(input), "copy_sshd_config", "Copy /etc/sshd_config from installer environment? (yes/no)", "yes"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; const char* file = "/etc/sshd_config"; textf("Copying %s -> %s\n", file, file + 1); execute((const char*[]) {"cp", file, file + 1}, "f"); has_sshd_config = true; break; } text("\n"); } if ( enabled_sshd && !has_sshd_config ) { text("Password authentication has been disabled by default in sshd to " "prevent remotely guessing insecure passwords. The recommended " "approach is to put your public key in the installation .iso and " "generate the sshd credentials ahead of time as documented in " "release-iso-modification(7). However, you could enable password " "authentication if you picked a very strong password.\n\n"); bool enable_sshd_password = false; while ( true ) { prompt(input, sizeof(input), "enable_sshd_password", "Enable sshd password authentication? (yes/no)", "no"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; if ( !install_configurationf("etc/sshd_config", "a", "PasswordAuthentication yes\n") ) { warn("etc/sshd_config"); continue; } enable_sshd_password = true; text("Added 'PasswordAuthentication yes' to /etc/sshd_config\n"); break; } while ( enable_sshd_password ) { prompt(input, sizeof(input), "enable_sshd_root_password", "Enable sshd password authentication for root? (yes/no)", "no"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; if ( !install_configurationf("etc/sshd_config", "a", "PermitRootLogin yes\n") ) { warn("etc/sshd_config"); continue; } text("Added 'PermitRootLogin yes' to /etc/sshd_config\n"); break; } text("\n"); } if ( any_sshd_keys ) { while ( true ) { const char* question = "Copy sshd private keys from installer environment? (yes/no)"; prompt(input, sizeof(input), "copy_sshd_private_keys", question, "yes"); if ( strcasecmp(input, "no") == 0 ) break; if ( strcasecmp(input, "yes") != 0 ) continue; for ( size_t i = 0; i < sshd_key_files_count; i++ ) { const struct sshd_key_file* file = &sshd_key_files[i]; if ( access_or_die(file->pri, F_OK) < 0 ) continue; textf("Copying %s -> %s\n", file->pri, file->pri + 1); execute((const char*[]) {"cp", file->pri, file->pri + 1, NULL }, "f"); textf("Copying %s -> %s\n", file->pub, file->pub + 1); execute((const char*[]) {"cp", file->pub, file->pub + 1, NULL }, "f"); } break; } text("\n"); } text("It's time to boot into the newly installed system.\n\n"); if ( strcasecmp(accept_grub, "no") == 0 ) text("You did not accept a bootloader and need to set up bootloading " "yourself. /etc/default/grub.d/10_sortix.cache is a GRUB " "configuration fragment that boots the newly installed system." "\n\n"); text("Upon boot, you'll be greeted with a login screen. Enter your " "credentials to get a command line. Login as user 'poweroff' as " "described in login(8) to power off the machine or run poweroff(8). " "After logging in, type 'man user-guide' to view the introductory " "documentation.\n"); text("\n"); while ( true ) { prompt(input, sizeof(input), "finally", "What now? (exit/poweroff/reboot/halt/boot/chroot)", "boot"); if ( !strcasecmp(input, "exit") ) exit(0); else if ( !strcasecmp(input, "poweroff") ) exit_gui(0); else if ( !strcasecmp(input, "reboot") ) exit_gui(1); else if ( !strcasecmp(input, "halt") ) exit_gui(2); else if ( !strcasecmp(input, "boot") ) { if ( !access("/etc/fstab", F_OK) ) { printf("Only a live environment can reinit installations.\n"); continue; } execute((const char*[]) {"mkdir", "-p", "/etc/init", NULL }, "ef"); execute((const char*[]) {"cp", "etc/fstab", "/etc/fstab", NULL }, "ef"); execute((const char*[]) {"sh", "-c", "echo 'require chain exit-code' > " "/etc/init/default", NULL }, "ef"); exit_gui(3); } else if ( !strcasecmp(input, "chroot") ) { unmount_all_but_root(); unsetenv("SYSINSTALL_TARGET"); unsetenv("SHLVL"); unsetenv("INIT_PID"); exit(execute((const char*[]) { "chroot", "-d", fs, "/sbin/init", NULL }, "f")); } } }