From 1c6254b50ffcd8334f00ec466d8f4f9286a1f146 Mon Sep 17 00:00:00 2001 From: Nocturn9x Date: Thu, 2 Dec 2021 14:02:05 +0100 Subject: [PATCH] Improved README --- README.md | 53 ++++++++++++++++-- src/core/mainloop.nim | 5 +- src/util/disks.nim | 125 ++++++++++++++++++++++++++++++++---------- src/util/misc.nim | 10 ++-- 4 files changed, 153 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 47a6fbc..85cdfc8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,50 @@ -# nimd -A minimal, self-contained dependency-based Linux init system written in Nim +# Nim Daemon - An init system written in Nim +A minimal, self-contained, dependency-based Linux init system written in Nim. + +## Note + +The code in here is pretty bad: in fact, it's horrible (and it _barely_ works). I'm just messing around to get the basics done, sorry for that, +but I need to get a proof of concept done before starting to do some actually sensible programming. + +I mainly made this thing for fun and as an excuse to learn more about the mysterious PID 1 and the Linux kernel in general: if you are like me +and love getting your hands dirty, I truly recommend trying an endeavor like this as I haven't had this fun cobbling something together in a very +long time. Sometimes programming only large scale software is boring, go figure. + +## Disclaimers & Functionality + +This software is developed on a _"It works on my machine"_ basis. I don't perform any extensive testing: if this thing unmounts your +root partition while you're saving your precious family photos, I can't do much. I run an installation of Artix Linux (x86_64) +using the 5.15.5-artix1-1 linux kernel. + +Also, NimD is developed **for Linux only**, as that's the kernel my OS uses: other kernels are not supported at all and NimD +**will** explode with fancy fireworks if you try to run it unmodified on other kernels (although probably things like BSD and Solaris +are not that hard to add support for using some `when defined()` clauses and changing what virtual filesystems NimD expects to mount) + +NimD is not particularly secure. Actually it's probably very insecure by modern standards, but basic checks like making sure regular users +can't reboot the machine are (_actually_, will be) at least in place, so there's that I guess. + +NimD assumes that the standard file descriptors 0, 1 and 2 (stdin, stdout and stderr respectively) are properly connected to /dev/console +(which is something all modern versions of the Linux kernel do). I tried connecting them manually, but I was out of luck: if you happen to +know how to check for a proper set of file descriptors and connect them manually, please make a PR, I'd love to hear how to do that. + +When mounting the filesystem, NimD is at least somewhat smart: +- First, it'll try to mount the standard POSIX virtual filesystems (/proc, /sys, etc) if they're not mounted already +- Then, it'll parse /etc/fstab and mount all the disks from there as well (unless they are already mounted, of course). + Drive IDs/UUIDs, LABELs and PARTUUIDs are also supported and are automatically resolved to their respective /dev/disk/by-XXX symlink + +__Note__: To check if a disk is mounted, NimD reads /proc/mounts. If said virtual file is absent (say because we just booted and haven't mounted +/proc yet), the disk is assumed to be unmounted and is then mounted. This seems fairly reasonable to me, but in the off chance that said disk is +indeed mounted and NimD doesn't know it, it'll just log the error about the disk being already mounted and happily continue its merry way into +booting your system (_hopefully_), but failing to mount any of the POSIX virtual filesystems will cause NimD to abort with error code 131 (which +in turn will most likely cause your kernel to panic) because it's almost sure that the system would be in a broken state anyway. + +The way I envision NimD being installed on a system is the following: +- /etc/nimd -> Contains configuration files + - /etc/nimd/runlevels -> Contains the runlevels (think openrc) +- /var/run/nimd.sock -> Unix domain socket for IPC +- /etc/runlevels -> Symlink to /etc/nimd/runlevels +- /sbin/nimd -> Actual NimD executable +- /sbin/init -> Symlink to /sbin/nimd + -__Note__: This code is pretty bad. I'm just messing around to get the basics done right now so this code -is pretty much trash. Sorry for that, that's not how I usually work but I need to get a proof of concept -done before starting to do some actually sensible programming diff --git a/src/core/mainloop.nim b/src/core/mainloop.nim index e00aafe..c58115b 100644 --- a/src/core/mainloop.nim +++ b/src/core/mainloop.nim @@ -22,7 +22,7 @@ import ../util/[logging, disks, misc] proc mainLoop*(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") = ## NimD's main execution loop try: - addShutdownHandler(unmountAllDisks) + addShutdownHandler(unmountAllDisks, logger) if mountDisks: logger.info("Mounting filesystem") logger.info("Mounting virtual disks") @@ -44,6 +44,9 @@ proc mainLoop*(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fs try: # TODO sleepSeconds(5) + except CtrlCException: + logger.warning("Main process received SIGINT: exiting") + nimDExit(logger, 130) # 130 - Interrupted by SIGINT except: logger.critical(&"A critical error has occurred while running, restarting the mainloop! Error -> {getCurrentExceptionMsg()}") # We *absolutely* cannot die diff --git a/src/util/disks.nim b/src/util/disks.nim index abbf3f7..e7380fc 100644 --- a/src/util/disks.nim +++ b/src/util/disks.nim @@ -21,50 +21,82 @@ import logging import misc -const virtualFileSystems: seq[tuple[source: string, target: string, filesystemtype: string, mountflags: uint64, data: string]] = @[ - (source: "proc", target: ("/proc"), filesystemtype: ("proc"), mountflags: 0u64, data: "nosuid,noexec,nodev"), - (source: ("sys"), target: ("/sys"), filesystemtype: ("sysfs"), mountflags: 0u64, data: ("nosuid,noexec,nodev")), - (source: ("run"), target: ("/run"), filesystemtype: ("tmpfs"), mountflags: 0u64, data: ("mode=0755,nosuid,nodev")), - (source: ("dev"), target: ("/dev"), filesystemtype: ("devtmpfs"), mountflags: 0u64, data: ("mode=0755,nosuid")), - (source: ("devpts"), target: ("/dev/pts"), filesystemtype: ("devpts"), mountflags: 0u64, data: ("mode=0620,gid=5,nosuid,noexec")), - (source: ("shm"), target: ("/dev/shm"), filesystemtype: ("tmpfs"), mountflags: 0u64, data: ("mode=1777,nosuid,nodev")), - +const virtualFileSystems: array[6, tuple[source: string, target: string, filesystemtype: string, mountflags: uint64, data: string, dump, pass: uint8]] = [ + # ALl the standard POSIX filesystems needed by the OS to work properly are listed here + (source: "proc", target: "/proc", filesystemtype: "proc", mountflags: 0u64, data: "nosuid,noexec,nodev", dump: 0u8, pass: 0u8), + (source: "sys", target: "/sys", filesystemtype: "sysfs", mountflags: 0u64, data: "nosuid,noexec,nodev", dump: 0u8, pass: 0u8), + (source: "run", target: "/run", filesystemtype: "tmpfs", mountflags: 0u64, data: "mode=0755,nosuid,nodev", dump: 0u8, pass: 0u8), + (source: "dev", target: "/dev", filesystemtype: "devtmpfs", mountflags: 0u64, data: "mode=0755,nosuid", dump: 0u8, pass: 0u8), + (source: "devpts", target: "/dev/pts", filesystemtype: "devpts", mountflags: 0u64, data: "mode=0620,gid=5,nosuid,noexec", dump: 0u8, pass: 0u8), + (source: "shm", target: "/dev/shm", filesystemtype: "tmpfs", mountflags: 0u64, data: "mode=1777,nosuid,nodev", dump: 0u8, pass: 0u8), ] -proc parseFileSystemTable*(fstab: string): seq[tuple[source, target, filesystemtype: string, mountflags: uint64, data: string]] = + +proc parseFileSystemTable*(fstab: string): seq[tuple[source, target, filesystemtype: string, mountflags: uint64, data: string, dump, pass: uint8]] = ## Parses the contents of the given file (the contents of /etc/fstab or /etc/mtab ## most of the time, but this is not enforced in any way) and returns a sequence - ## of tuples with elements source, target, filesystemtype, mountflags and data as - ## required by the mount system call. + ## of tuples with elements source, target, filesystemtype, mountflags, data, dump + ## and pass as required by mount/umount/umount2 in sys/mount.h which are wrapped below. ## The types of these arguments are Nim types to make the garbage collector happy ## and avoid freeing the underlying string object. - ## as required by mount/umount/umount2 in sys/mount.h which is wrapped below. - ## An improperly formatted fstab will cause this function to error out with an - ## IndexDefect exception (when an entry is incomplete) that should be caught by + ## An improperly formatted fstab will cause this function to error out with a + ## ValueError exception (when an entry is incomplete) that should be caught by ## the caller. No other checks other than very basic syntax are performed, as - ## that job is delegated to the operating system. + ## that job is delegated to the operating system. Missing dump/pass entries are + ## interpreted as if they were set to 0, following the way Linux does it. ## Note that this function automatically converts UUID/LABEL/PARTUUID/ID directives - ## to their corresponding symlink just like the mount command would do on a Linux system. + ## to their corresponding /dev/disk symlink just like the mount command would do + ## on a Linux system. var temp: seq[string] = @[] + var dump: int + var pass: int var line: string = "" for l in fstab.splitlines(): line = l.strip().replace("\t", " ") - if line.startswith("#"): - continue - if line.isEmptyOrWhitespace(): + if line.startswith("#") or line.isEmptyOrWhitespace(): continue # This madness will make sure we only get (hopefully) 6 entries # in our temporary list temp = line.split().filterIt(it != "").join(" ").split(maxsplit=6) + if len(temp) < 6: + if len(temp) < 4: + # Not enough columns! + raise newException(ValueError, "improperly formatted filesystem table") + elif len(temp) == 4: + dump = 0 + pass = 0 + elif len(temp) == 5: + dump = 0 + else: + try: + dump = parseInt(temp[4]) + except ValueError: + raise newException(ValueError, &"improperly formatted filesystem table -> invalid value ({dump}) for dump") + try: + pass = parseInt(temp[5]) + except ValueError: + raise newException(ValueError, &"improperly formatted filesystem table -> invalid value ({pass}) for pass") + if dump notin 0..1: + raise newException(ValueError, &"invalid value in filesystem table -> invalid value ({dump}) for dump") + if pass notin 0..2: + raise newException(ValueError, &"invalid value in filesystem table -> invalid value ({pass}) for pass") if temp[0].toLowerAscii().startswith("id="): + if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): + raise newException(ValueError, "improperly formatted filesystem table") temp[0] = &"""/dev/disk/by-id/{temp[0].split("=", maxsplit=2)[1]}""" if temp[0].toLowerAscii().startswith("label="): + if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): + raise newException(ValueError, "improperly formatted filesystem table") temp[0] = &"""/dev/disk/by-label/{temp[0].split("=", maxsplit=2)[1]}""" if temp[0].toLowerAscii().startswith("uuid="): + if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): + raise newException(ValueError, "improperly formatted filesystem table") temp[0] = &"""/dev/disk/by-uuid/{temp[0].split("=", maxsplit=2)[1]}""" if temp[0].toLowerAscii().startswith("partuuid="): + if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): + raise newException(ValueError, "improperly formatted filesystem table") temp[0] = &"""/dev/disk/by-partuuid/{temp[0].split("=", maxsplit=2)[1]}""" - result.add((source: temp[0], target: temp[1], filesystemtype: temp[2], mountflags: 0u64, data: temp[3])) + result.add((source: temp[0], target: temp[1], filesystemtype: temp[2], mountflags: 0u64, data: temp[3], dump: uint8(dump), pass: uint8(pass))) # Nim wrappers around C functionality in sys/mount.h on Linux @@ -82,6 +114,7 @@ proc umount2*(target: cstring, flags: cint): cint {.header: "sys/mount.h", impor proc umount*(target: string): int = int(umount(cstring(target))) proc umount2*(target: string, flags: int): int = int(umount2(cstring(target), cint(flags))) + proc exists(p: string): bool = # Checks if a path exists. Thanks # araq :) @@ -92,12 +125,19 @@ proc exists(p: string): bool = result = false -proc checkDisksIsMounted(search: tuple[source, target, filesystemtype: string, mountflags: uint64, data: string], expand: bool = false): bool = - ## Returns true if a disk is already mounted +proc checkDisksIsMounted(search: tuple[source, target, filesystemtype: string, mountflags: uint64, data: string, dump, pass: uint8], expand: bool = false): bool = + ## Returns true if a disk is already mounted. If expand is true, + ## symlinks are expanded and checked instead of doing a simple + ## string comparison of the source entry point. This should be + ## true when mounting real filesystems. This returns false if + ## /proc/mounts does not exist (usually happens when /proc has + ## not been mounted yet) + if not fileExists("/proc/mounts"): + return false for entry in parseFileSystemTable(readFile("/proc/mounts")): if expand: - if exists(entry.source) and exists(search.source) and sameFile(entry.source, search.source): - return true + if exists(entry.source) and exists(search.source) and sameFile(entry.source, search.source): + return true elif entry.source == search.source: return true return false @@ -106,7 +146,7 @@ proc checkDisksIsMounted(search: tuple[source, target, filesystemtype: string, m proc mountRealDisks*(logger: Logger, fstab: string = "/etc/fstab") = ## Mounts real disks from /etc/fstab try: - logger.info(&"Reading disk entries from {fstab}") + logger.debug(&"Reading disk entries from {fstab}") for entry in parseFileSystemTable(readFile(fstab)): if checkDisksIsMounted(entry, expand=true): logger.debug(&"Skipping mounting filesystem {entry.source} ({entry.filesystemtype}) at {entry.target}: already mounted") @@ -121,7 +161,7 @@ proc mountRealDisks*(logger: Logger, fstab: string = "/etc/fstab") = posix.errno = cint(0) else: logger.debug(&"Mounted {entry.source} at {entry.target}") - except IndexDefect: # Check parseFileSystemTable for more info on this catch block + except ValueError: # Check parseFileSystemTable for more info on this catch block logger.fatal("Improperly formatted fstab, exiting") nimDExit(logger, 131) @@ -150,15 +190,40 @@ proc mountVirtualDisks*(logger: Logger) = proc unmountAllDisks*(logger: Logger, code: int) = ## Unmounts all currently mounted disks, including the ones that ## were not mounted trough fstab and virtual filesystems + var flag: bool = false try: - logger.info(&"Reading disk entries from /proc/mounts") + logger.info("Detaching real filesystems") + logger.debug(&"Reading disk entries from /proc/mounts") for entry in parseFileSystemTable(readFile("/proc/mounts")): + flag = false + if entry.source in ["proc", "sys", "run", "dev", "devpts", "shm"]: + continue # We cannot detach the vfs just yet, we'll do it later + for path in ["/proc", "/sys", "/run", "/dev", "/dev/pts", "/dev/shm"]: + if entry.target.startswith(path): + flag = true + if flag: + continue if not checkDisksIsMounted(entry): logger.debug(&"Skipping unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target}: not mounted") continue logger.debug(&"Unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target}") - logger.trace(&"Calling umount('{entry.target}')") - var retcode = umount(entry.target) + logger.trace(&"Calling umount2('{entry.target}', MNT_DETACH)") + var retcode = umount2(entry.target, 2) # MNT_DETACH - Since we're shutting down, we need the disks to be *gone*! + logger.trace(&"umount2('{entry.target}', MNT_DETACH) returned {retcode}") + if retcode == -1: + logger.error(&"Unmounting disk {entry.source} from {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") + # Resets the error code + posix.errno = cint(0) + else: + logger.debug(&"Unmounted {entry.source} from {entry.target}") + logger.info("Detaching virtual filesystems") + for entry in virtualFileSystems: + if not checkDisksIsMounted(entry): + logger.debug(&"Skipping unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target}: not mounted") + continue + logger.debug(&"Unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target}") + logger.trace(&"Calling umount2('{entry.target}', MNT_DETACH)") + var retcode = umount2(entry.target, 2) # MNT_DETACH - Since we're shutting down, we need the disks to be *gone*! logger.trace(&"umount('{entry.target}') returned {retcode}") if retcode == -1: logger.error(&"Unmounting disk {entry.source} from {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") @@ -166,6 +231,6 @@ proc unmountAllDisks*(logger: Logger, code: int) = posix.errno = cint(0) else: logger.debug(&"Unmounted {entry.source} from {entry.target}") - except IndexDefect: # Check parseFileSystemTable for more info on this catch block + except ValueError: # Check parseFileSystemTable for more info on this catch block logger.fatal("Improperly formatted /etc/mtab, exiting") nimDExit(logger, 131) diff --git a/src/util/misc.nim b/src/util/misc.nim index 87d70bb..cb4d876 100644 --- a/src/util/misc.nim +++ b/src/util/misc.nim @@ -20,17 +20,20 @@ import strformat import logging +type CtrlCException* = object of CatchableError var shutdownHandlers: seq[proc (logger: Logger, code: int)] = @[] -proc addShutdownHandler*(handler: proc (logger: Logger, code: int)) = +proc addShutdownHandler*(handler: proc (logger: Logger, code: int), logger: Logger) = shutdownHandlers.add(handler) proc removeShutdownHandler*(handler: proc (logger: Logger, code: int)) = - shutdownHandlers.delete(shutdownHandlers.find(handler)) + for i, h in shutdownHandlers: + if h == handler: + shutdownHandlers.delete(i) proc nimDExit*(logger: Logger, code: int) = @@ -58,5 +61,4 @@ proc sleepSeconds*(amount: SomeInteger) = sleep(amount * 1000) proc handleControlC* {.noconv.} = - getDefaultLogger().warning("Main process received SIGINT: exiting") # TODO: Call exit point - nimDExit(getDefaultLogger(), 130) # Exit code 130 indicates a SIGINT \ No newline at end of file + raise newException(CtrlCException, "Interrupted by Ctrl+C")