aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-05-26 19:16:32 +0900
committerOphestra <cat@gensokyo.uk>2026-05-27 10:37:45 +0900
commit8c7109a2d58c40b86ad97ed520aff6b00bbf1667 (patch)
tree547f611b77e98f5df433ba513cbcbc9dc47e4b8e /cmd
parentcaf3e0db4b92d859599ee199b32051fae8c99016 (diff)
cmd/earlyinit: mount system device
This currently relies on a trusted bootloader to determine the boot device. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/earlyinit/main.go149
-rw-r--r--cmd/earlyinit/mount.go69
-rw-r--r--cmd/earlyinit/uevent.go103
-rw-r--r--cmd/earlyinit/uevent_test.go35
4 files changed, 336 insertions, 20 deletions
diff --git a/cmd/earlyinit/main.go b/cmd/earlyinit/main.go
index 0e17d78c..df454e3d 100644
--- a/cmd/earlyinit/main.go
+++ b/cmd/earlyinit/main.go
@@ -5,17 +5,88 @@
package main
import (
+ "context"
+ "crypto/rand"
"log"
"os"
+ "os/signal"
"runtime"
+ "runtime/pprof"
+ "slices"
"strings"
. "syscall"
+
+ "hakurei.app/internal/kobject"
+ "hakurei.app/internal/report"
+ "hakurei.app/internal/uevent"
)
-func main() {
- runtime.LockOSThread()
+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()
var (
option map[string]string
@@ -33,15 +104,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 +187,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 +238,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)
+ })
+}