Improved README

This commit is contained in:
Nocturn9x 2021-12-02 14:02:05 +01:00
parent 1cebcc0d90
commit 1c6254b50f
4 changed files with 153 additions and 40 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
raise newException(CtrlCException, "Interrupted by Ctrl+C")