From f03c0fb249ea8787ae8ec086970eb089109ebf6f Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 30 Mar 2026 21:20:42 +0900 Subject: [PATCH] internal/uevent: synthetic events for coldboot This causes the kernel to regenerate events that happened before earlyinit started. Signed-off-by: Ophestra --- internal/pkg/dir_test.go | 25 ++++ internal/uevent/coldboot.go | 71 ++++++++++ internal/uevent/coldboot_test.go | 227 +++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 internal/uevent/coldboot.go create mode 100644 internal/uevent/coldboot_test.go diff --git a/internal/pkg/dir_test.go b/internal/pkg/dir_test.go index 98e6ab4a..cceb89b1 100644 --- a/internal/pkg/dir_test.go +++ b/internal/pkg/dir_test.go @@ -27,6 +27,31 @@ func TestFlatten(t *testing.T) { fs.ModeCharDevice | 0400, )}, + {"coldboot", fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "devices": {Mode: fs.ModeDir | 0700}, + "devices/uevent": {Mode: 0600, Data: []byte("add")}, + "devices/empty": {Mode: fs.ModeDir | 0700}, + + "devices/sub": {Mode: fs.ModeDir | 0700}, + "devices/sub/uevent": {Mode: 0600, Data: []byte("add")}, + + "block": {Mode: fs.ModeDir | 0700}, + "block/uevent": {Mode: 0600, Data: []byte{}}, + }, []pkg.FlatEntry{ + {Mode: fs.ModeDir | 0700, Path: "."}, + + {Mode: fs.ModeDir | 0700, Path: "block"}, + {Mode: 0600, Path: "block/uevent", Data: []byte{}}, + + {Mode: fs.ModeDir | 0700, Path: "devices"}, + {Mode: fs.ModeDir | 0700, Path: "devices/empty"}, + {Mode: fs.ModeDir | 0700, Path: "devices/sub"}, + {Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")}, + {Mode: 0600, Path: "devices/uevent", Data: []byte("add")}, + }, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil}, + {"empty", fstest.MapFS{ ".": {Mode: fs.ModeDir | 0700}, "checksum": {Mode: fs.ModeDir | 0700}, diff --git a/internal/uevent/coldboot.go b/internal/uevent/coldboot.go new file mode 100644 index 00000000..07f6497f --- /dev/null +++ b/internal/uevent/coldboot.go @@ -0,0 +1,71 @@ +package uevent + +import ( + "context" + "errors" + "io/fs" + "log" + "os" + "path/filepath" +) + +// synthAdd is prepared bytes written to uevent to cause a synthetic add event +// to be emitted during coldboot. +var synthAdd = []byte(KOBJ_ADD.String()) + +// Coldboot writes "add" to every uevent file that it finds in /sys/devices. +// This causes the kernel to regenerate the uevents for these paths. +// +// The specified pathname must present the sysfs root. +// +// Note that while [AOSP documentation] claims to also scan /sys/class and +// /sys/block, this is no longer the case, and the documentation was not updated +// when this changed. +// +// [AOSP documentation]: https://android.googlesource.com/platform/system/core/+/master/init/README.ueventd.md +func Coldboot( + ctx context.Context, + pathname string, + visited chan<- string, + handleWalkErr func(error) error, +) error { + if handleWalkErr == nil { + handleWalkErr = func(err error) error { + if errors.Is(err, fs.ErrNotExist) { + log.Println("coldboot", err) + return nil + } + return err + } + } + + return filepath.WalkDir(filepath.Join(pathname, "devices"), func( + path string, + d fs.DirEntry, + err error, + ) error { + if err != nil { + return handleWalkErr(err) + } + if err = ctx.Err(); err != nil { + return err + } + + if d.IsDir() || d.Name() != "uevent" { + return nil + } + if err = os.WriteFile(path, synthAdd, 0); err != nil { + return handleWalkErr(err) + } + + select { + case visited <- path: + break + + case <-ctx.Done(): + return ctx.Err() + } + + return nil + }) +} diff --git a/internal/uevent/coldboot_test.go b/internal/uevent/coldboot_test.go new file mode 100644 index 00000000..e3491e64 --- /dev/null +++ b/internal/uevent/coldboot_test.go @@ -0,0 +1,227 @@ +package uevent_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "slices" + "sync" + "syscall" + "testing" + + "hakurei.app/check" + "hakurei.app/internal/pkg" + "hakurei.app/internal/uevent" +) + +func TestColdboot(t *testing.T) { + t.Parallel() + + d := t.TempDir() + if err := os.Chmod(d, 0700); err != nil { + t.Fatal(err) + } + + for _, s := range []string{ + "devices", + "devices/sub", + "devices/empty", + "block", + } { + if err := os.MkdirAll(filepath.Join(d, s), 0700); err != nil { + t.Fatal(err) + } + } + + for _, f := range [][2]string{ + {"devices/uevent", ""}, + {"devices/sub/uevent", ""}, + {"block/uevent", ""}, + } { + if err := os.WriteFile( + filepath.Join(d, f[0]), + []byte(f[1]), + 0600, + ); err != nil { + t.Fatal(err) + } + } + + var wg sync.WaitGroup + defer wg.Wait() + + visited := make(chan string) + var got []string + wg.Go(func() { + for path := range visited { + got = append(got, path) + } + }) + + err := uevent.Coldboot(t.Context(), d, visited, func(err error) error { + t.Errorf("handleWalkErr: %v", err) + return err + }) + close(visited) + if err != nil { + t.Fatalf("Coldboot: error = %v", err) + } + + wg.Wait() + want := []string{ + "devices/sub/uevent", + "devices/uevent", + } + for i, rel := range want { + want[i] = filepath.Join(d, rel) + } + if !slices.Equal(got, want) { + t.Errorf("Coldboot: %#v, want %#v", got, want) + } + + var checksum pkg.Checksum + if err = pkg.HashDir(&checksum, check.MustAbs(d)); err != nil { + t.Fatalf("HashDir: error = %v", err) + } + + wantChecksum := pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8") + if checksum != wantChecksum { + t.Errorf( + "Coldboot: checksum = %s, want %s", + pkg.Encode(checksum), + pkg.Encode(wantChecksum), + ) + } +} + +func TestColdbootError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + dF func(t *testing.T, d string) (wantErr error) + vF func(<-chan string, context.Context, context.CancelFunc) + hF func(d string, err error) error + }{ + {"walk", func(t *testing.T, d string) (wantErr error) { + wantErr = &os.PathError{ + Op: "open", + Path: filepath.Join(d, "devices"), + Err: syscall.EACCES, + } + if err := os.Mkdir(filepath.Join(d, "devices"), 0); err != nil { + t.Fatal(err) + } + return + }, nil, nil}, + + {"write", func(t *testing.T, d string) (wantErr error) { + wantErr = &os.PathError{ + Op: "open", + Path: filepath.Join(d, "devices/uevent"), + Err: syscall.EACCES, + } + if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil { + t.Fatal(err) + } else if err = os.WriteFile(filepath.Join(d, "devices/uevent"), nil, 0); err != nil { + t.Fatal(err) + } + return + }, nil, nil}, + + {"deref", func(t *testing.T, d string) (wantErr error) { + if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil { + t.Fatal(err) + } else if err = os.Symlink("/proc/nonexistent", filepath.Join(d, "devices/uevent")); err != nil { + t.Fatal(err) + } + return + }, nil, nil}, + + {"deref handle", func(t *testing.T, d string) (wantErr error) { + if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil { + t.Fatal(err) + } else if err = os.Symlink("/proc/nonexistent", filepath.Join(d, "devices/uevent")); err != nil { + t.Fatal(err) + } + return + }, nil, func(d string, err error) error { + if reflect.DeepEqual(err, &os.PathError{ + Op: "open", + Path: filepath.Join(d, "devices/uevent"), + Err: syscall.ENOENT, + }) { + return nil + } + return err + }}, + + {"cancel early", func(t *testing.T, d string) (wantErr error) { + wantErr = context.Canceled + if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil { + t.Fatal(err) + } + return + }, func(visited <-chan string, ctx context.Context, cancel context.CancelFunc) { + if visited == nil { + cancel() + } + return + }, nil}, + + {"cancel", func(t *testing.T, d string) (wantErr error) { + wantErr = context.Canceled + if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil { + t.Fatal(err) + } else if err = os.WriteFile(filepath.Join(d, "devices/uevent"), nil, 0600); err != nil { + t.Fatal(err) + } else if err = os.Mkdir(filepath.Join(d, "devices/sub"), 0700); err != nil { + t.Fatal(err) + } else if err = os.WriteFile(filepath.Join(d, "devices/sub/uevent"), nil, 0600); err != nil { + t.Fatal(err) + } + return + }, func(visited <-chan string, ctx context.Context, cancel context.CancelFunc) { + if visited == nil { + return + } + <-visited + cancel() + return + }, nil}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + d := t.TempDir() + wantErr := tc.dF(t, d) + + var wg sync.WaitGroup + defer wg.Wait() + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + var visited chan string + if tc.vF != nil { + tc.vF(nil, ctx, cancel) + visited = make(chan string) + defer close(visited) + wg.Go(func() { tc.vF(visited, ctx, cancel) }) + } + + var handleWalkErr func(error) error + if tc.hF != nil { + handleWalkErr = func(err error) error { + return tc.hF(d, err) + } + } + + if err := uevent.Coldboot(ctx, d, visited, handleWalkErr); !reflect.DeepEqual(err, wantErr) { + t.Errorf("Coldboot: error = %v, want %v", err, wantErr) + } + }) + } +}