From 3c4ae383abcf10ad30a8646df24a9b5ad9f54148 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 26 May 2026 19:16:32 +0900 Subject: [PATCH] cmd/earlyinit: mount system device This currently relies on a trusted bootloader to determine the boot device. Signed-off-by: Ophestra --- cmd/earlyinit/main.go | 150 ++++++++++++++++++++++++++++++----- cmd/earlyinit/mount.go | 69 ++++++++++++++++ cmd/earlyinit/uevent.go | 103 ++++++++++++++++++++++++ cmd/earlyinit/uevent_test.go | 35 ++++++++ 4 files changed, 337 insertions(+), 20 deletions(-) create mode 100644 cmd/earlyinit/mount.go create mode 100644 cmd/earlyinit/uevent.go create mode 100644 cmd/earlyinit/uevent_test.go diff --git a/cmd/earlyinit/main.go b/cmd/earlyinit/main.go index 0e17d78c..5fd9374c 100644 --- a/cmd/earlyinit/main.go +++ b/cmd/earlyinit/main.go @@ -5,17 +5,89 @@ package main import ( + "context" + "crypto/rand" "log" "os" + "os/exec" + "os/signal" "runtime" + "runtime/pprof" + "slices" "strings" . "syscall" + + "hakurei.app/internal/kobject" + "hakurei.app/internal/report" + "hakurei.app/internal/uevent" +) + +var r report.Reporter + +func init() { + log.SetFlags(0) + log.SetPrefix("earlyinit: ") + r.SetOutput(log.Default()) + + // this handles SIGQUIT to provide useful debugging information without + // terminating, and prevents the runtime from throwing on the must family + // of early error reporting functions, DO NOT REMOVE + c := make(chan os.Signal, 1) + signal.Notify(c, SIGQUIT) + go func() { + for { + <-c + if p := pprof.Lookup("goroutine"); p == nil { + log.Println("initial built-in goroutine profile does not exist") + } else if err := p.WriteTo(os.Stderr, 2); err != nil { + log.Println(err) + } + } + }() +} + +// fatal calls [log.Println] with v and blocks forever. Must be called from +// main. Must not be used after error reporting is set up. +func fatal(v ...any) { + log.Println(v...) + log.Println("unable to continue, please reboot and resolve the problem manually") + select {} +} + +// must calls fatal with err if it is non-nil. +func must(err error) { + if err != nil { + log.Println(err) + select {} + } +} + +// mustSyscall is like must, but with an additional action name. +func mustSyscall(action string, err error) { + if err != nil { + fatal("cannot "+action+":", err) + select {} + } +} + +// must1 is like must, but with an additional passed through value. +func must1[T any](v T, err error) T { + must(err) + return v +} + +const ( + // optionSystem specifies devpath of the system device. + optionSystem = "system" + + // flagStrict sets [report.DStrict] on r. + flagStrict = "strict" + // flagNoRecover sets [report.DNoRecover] on r. + flagNoRecover = "no_recover" ) func main() { runtime.LockOSThread() - log.SetFlags(0) - log.SetPrefix("earlyinit: ") var ( option map[string]string @@ -33,15 +105,33 @@ func main() { } } - if err := Mount( + { + var flag uint64 + if slices.Contains(flags, flagStrict) { + flag |= report.DStrict + } + if slices.Contains(flags, flagNoRecover) { + flag |= report.DNoRecover + } + log.Printf("reporting flags %x", flag) + r.SetFlags(flag) + } + + mustSyscall("mount devtmpfs", Mount( "devtmpfs", "/dev/", "devtmpfs", MS_NOSUID|MS_NOEXEC, "", - ); err != nil { - log.Fatalf("cannot mount devtmpfs: %v", err) - } + )) + must(os.Mkdir("/dev/pts/", 0)) + mustSyscall("mount devpts", Mount( + "devpts", + "/dev/pts/", + "devpts", + MS_NOSUID|MS_NOEXEC, + "mode=620,ptmxmode=666", + )) // The kernel might be unable to set up the console. When that happens, // printk is called with "Warning: unable to open an initial console." @@ -98,6 +188,40 @@ func main() { "", )) + conn := must1(uevent.Dial(-128 * 1024 * 1024)) + events := make(chan *uevent.Message, 1<<10) + var uuid uevent.UUID + must1(rand.Read(uuid[:])) + ctx, cancel := context.WithCancel(context.Background()) + + go consume(ctx, &r, conn, uuid, events) + s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) { + log.Printf("change %s: %q", o.DevPath, env) + }, func(err error) { + r.Dispatch( + report.Inconsistent, + "processed inconsistent uevent", + err, + ) + }) + go func() { + s.Consume(ctx, events) + + log.Println("closing NETLINK_KOBJECT_UEVENT socket") + cancel() + if err := conn.Close(); err != nil { + log.Fatal(err) // not reached + } + }() + + must(os.Mkdir("/system", 0)) + if devpath := option[optionSystem]; devpath == "" { + fatal("system must be nonempty") + } else { + log.Printf("waiting for devpath pattern %q", devpath) + mustMountSystem(ctx, s, devpath) + } + // after top level has been set up mustSyscall("remount root", Mount( "", @@ -115,17 +239,3 @@ func main() { )) } - -// mustSyscall calls [log.Fatalln] if err is non-nil. -func mustSyscall(action string, err error) { - if err != nil { - log.Fatalln("cannot "+action+":", err) - } -} - -// must calls [log.Fatal] with err if it is non-nil. -func must(err error) { - if err != nil { - log.Fatal(err) - } -} diff --git a/cmd/earlyinit/mount.go b/cmd/earlyinit/mount.go new file mode 100644 index 00000000..6ac59a46 --- /dev/null +++ b/cmd/earlyinit/mount.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "errors" + "os" + "path/filepath" + "strconv" + "syscall" + "time" + + "hakurei.app/check" + "hakurei.app/fhs" + "hakurei.app/internal/kobject" +) + +// mustMountSystem waits for and mounts a system device matching pattern. +func mustMountSystem( + ctx context.Context, + s *kobject.State, + pattern string, +) { + c, stop := context.WithTimeout(ctx, 30*time.Second) + defer stop() + + for { + var matchErr error + var systemPath *check.Absolute + s.Range(c, func(o *kobject.Object) bool { + if o.Subsystem != "block" || + o.Env["DEVTYPE"] != "disk" { + return true + } + + if ok, err := filepath.Match(pattern, o.DevPath); err != nil { + matchErr = err + return false + } else if !ok { + return true + } + + name, ok := o.Env["DEVNAME"] + if !ok { + return true + } + systemPath = fhs.AbsDev.Append(name) + return false + }) + if c.Err() != nil { + fatal("devpath", strconv.Quote(pattern), "never appeared") + } + if matchErr != nil { + fatal("cannot match system devpath:", matchErr) + } + err := syscall.Mount( + systemPath.String(), + "/system/", + "squashfs", + 0, + "threads=multi", + ) + if err == nil { + break + } + if !errors.Is(err, os.ErrNotExist) { + fatal("cannot mount system:", err) + } + } +} diff --git a/cmd/earlyinit/uevent.go b/cmd/earlyinit/uevent.go new file mode 100644 index 00000000..0ab134cf --- /dev/null +++ b/cmd/earlyinit/uevent.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "log" + "time" + + "hakurei.app/fhs" + "hakurei.app/internal/report" + "hakurei.app/internal/uevent" +) + +// newRejectColdboot returns a function to be called on every subsequent pending +// coldboot, and returns whether coldboot should proceed. Rejection is sticky. +func newRejectColdboot() func() bool { + // one coldboot per five minutes, two consecutive coldboot + const ( + coldbootInterval = 5 * time.Minute + coldbootBurst = 2 + ) + + done := make(chan struct{}) + s := make(chan struct{}, coldbootBurst) + s <- struct{}{} // for early fault before reporting is ready + go func() { + t := time.NewTicker(coldbootInterval) + for { + select { + case <-done: + return + + case <-t.C: + select { + case s <- struct{}{}: + default: + } + } + } + }() + + return func() bool { + select { + case <-s: + return true + + case <-done: + return false + + default: + close(done) + return false + } + } +} + +// consume continuously consumes events from conn with retries. +func consume( + ctx context.Context, + r *report.Reporter, + conn *uevent.Conn, + uuid uevent.UUID, + events chan<- *uevent.Message, +) { + defer close(events) + + nextColdboot := newRejectColdboot() + coldboot := true +retry: + if dispatchErr := conn.Consume(ctx, fhs.Sys, &uuid, events, coldboot, func(path string) { + log.Println("coldboot visited", path) + }, func(err error) bool { + if _, ok := err.(uevent.NeedsColdboot); ok && !nextColdboot() { + r.Dispatch( + report.Degraded, + "rejecting coldboot loop", + err, + ) + return false + } + r.Dispatch( + report.Inconsistent, + "consumed invalid message", + err, + ) + return true + }, nil); dispatchErr != nil { + if _, ok := dispatchErr.(uevent.Recoverable); !ok { + r.Dispatch( + report.Fatal, + "discontinuing uevent processing due to nonrecoverable error", + dispatchErr, + ) + return + } + + if _, ok := dispatchErr.(uevent.NeedsColdboot); ok { + // coldboot loop rejected by handler + coldboot = false + } + + goto retry + } +} diff --git a/cmd/earlyinit/uevent_test.go b/cmd/earlyinit/uevent_test.go new file mode 100644 index 00000000..d4b77141 --- /dev/null +++ b/cmd/earlyinit/uevent_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "testing" + "testing/synctest" + "time" +) + +func TestRejectColdboot(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + nextColdboot := newRejectColdboot() + want := func(want bool) { + if got := nextColdboot(); got != want { + t.Fatalf("nextColdboot: %v, want %v", got, want) + } + } + + synctest.Wait() + want(true) + time.Sleep(time.Hour) + synctest.Wait() + want(true) + want(true) + time.Sleep(5 * time.Minute) + synctest.Wait() + want(true) + want(false) + time.Sleep(time.Hour) + synctest.Wait() + want(false) + want(false) + }) +}