diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-03-30 21:20:42 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-03-30 23:01:08 +0900 |
| commit | f03c0fb249ea8787ae8ec086970eb089109ebf6f (patch) | |
| tree | 5e89ba203112c5e3ca984da96b9dc60be19ade2a /internal/uevent | |
| parent | a6600be34ad812ff13c89f45c3cadaac6d994e67 (diff) | |
internal/uevent: synthetic events for coldboot
This causes the kernel to regenerate events that happened before earlyinit started.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/uevent')
| -rw-r--r-- | internal/uevent/coldboot.go | 71 | ||||
| -rw-r--r-- | internal/uevent/coldboot_test.go | 227 |
2 files changed, 298 insertions, 0 deletions
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) + } + }) + } +} |
