aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-03-28 00:10:50 +0900
committerOphestra <cat@gensokyo.uk>2026-03-28 00:49:34 +0900
commit713bff3eb0a77efff016acc02b6f10913810f887 (patch)
treea648ab320007bf1aa15ab2f55e7856713cf92e88
parent30f459e690ca561d3932cb6efa0cf14ea53c25b2 (diff)
internal/uevent: decode uevent messages
The wire format and behaviour is entirely undocumented. This is implemented by reading lib/kobject_uevent.c, with testdata collected from the internal/rosa kernel. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--internal/uevent/message.go137
-rw-r--r--internal/uevent/message_test.go126
-rw-r--r--internal/uevent/uevent_test.go84
3 files changed, 339 insertions, 8 deletions
diff --git a/internal/uevent/message.go b/internal/uevent/message.go
new file mode 100644
index 00000000..aecd5b8c
--- /dev/null
+++ b/internal/uevent/message.go
@@ -0,0 +1,137 @@
+package uevent
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+)
+
+// A Message represents a kernel message to userspace.
+type Message struct {
+ // alloc_uevent_skb: action_string
+ Action KobjectAction `json:"action"`
+ // alloc_uevent_skb: devpath
+ DevPath string `json:"devpath"`
+ // add_uevent_var: key value strings
+ Env []string `json:"env"`
+}
+
+// String returns a multiline user-facing string representation of [Message].
+func (msg *Message) String() string {
+ var buf strings.Builder
+ buf.WriteString(msg.Action.String() + " event")
+ if msg.DevPath != "" {
+ buf.WriteString(" on " + msg.DevPath)
+ }
+ buf.WriteString(":")
+ for _, s := range msg.Env {
+ buf.WriteString("\n" + s)
+ }
+ return buf.String()
+}
+
+var (
+ // zero is a single pre-allocated NUL character.
+ zero = []byte{0}
+
+ // sepHeader is the separator in a [Message] header.
+ sepHeader = []byte{'@'}
+)
+
+func (msg *Message) AppendBinary(b []byte) (_ []byte, err error) {
+ if b, err = msg.Action.AppendText(b); err != nil {
+ return
+ }
+ b = append(b, sepHeader...)
+ b = append(b, msg.DevPath...)
+ b = append(b, zero...)
+ for _, s := range msg.Env {
+ b = append(b, s...)
+ b = append(b, zero...)
+ }
+ return b, nil
+}
+
+func (msg *Message) MarshalBinary() ([]byte, error) {
+ return msg.AppendBinary(nil)
+}
+
+// MissingHeaderError is an invalid representation of [Message] which is missing
+// its header added by alloc_uevent_skb.
+type MissingHeaderError string
+
+var _ Recoverable = MissingHeaderError("")
+
+func (MissingHeaderError) recoverable() {}
+func (e MissingHeaderError) Error() string {
+ return "message " + strconv.Quote(string(e)) + " has no header"
+}
+
+// MessageError describes a malformed representation of [Message].
+type MessageError struct {
+ // Full offending data.
+ Data string `json:"data"`
+ // Offending section.
+ Section string `json:"section"`
+ // Part of header in Section.
+ Kind int `json:"kind"`
+}
+
+var _ Recoverable = new(MessageError)
+var _ Nontrivial = new(MessageError)
+
+const (
+ // MErrorKindHeaderSep denotes a message header missing its separator.
+ MErrorKindHeaderSep = iota
+ // MErrorKindFinalNUL denotes a message body missing its final NUL terminator.
+ MErrorKindFinalNUL
+)
+
+func (*MessageError) recoverable() {}
+func (*MessageError) nontrivial() {}
+func (e *MessageError) Error() string {
+ switch e.Kind {
+ case MErrorKindHeaderSep:
+ return "header " + strconv.Quote(e.Section) + " missing separator"
+ case MErrorKindFinalNUL:
+ return "entry " + strconv.Quote(e.Section) + " missing NUL"
+
+ default:
+ return "section " + strconv.Quote(e.Section) + " is invalid"
+ }
+}
+
+func (msg *Message) UnmarshalBinary(data []byte) error {
+ header, body, ok := bytes.Cut(data, zero)
+ if !ok {
+ return MissingHeaderError(data)
+ }
+
+ action_string, devpath, ok := bytes.Cut(header, sepHeader)
+ if !ok {
+ return &MessageError{string(data), string(header), MErrorKindHeaderSep}
+ }
+
+ if err := msg.Action.UnmarshalText(action_string); err != nil {
+ return err
+ }
+ msg.DevPath = string(devpath)
+
+ if len(body) == 0 {
+ msg.Env = nil
+ return nil
+ }
+ msg.Env = make([]string, 0, bytes.Count(body, zero))
+
+ var s []byte
+ for len(body) != 0 {
+ var r []byte
+ s, r, ok = bytes.Cut(body, zero)
+ if !ok {
+ return &MessageError{string(data), string(body), MErrorKindFinalNUL}
+ }
+ body = r
+ msg.Env = append(msg.Env, string(s))
+ }
+ return nil
+}
diff --git a/internal/uevent/message_test.go b/internal/uevent/message_test.go
new file mode 100644
index 00000000..ffc7b949
--- /dev/null
+++ b/internal/uevent/message_test.go
@@ -0,0 +1,126 @@
+package uevent_test
+
+import (
+ "syscall"
+ "testing"
+
+ "hakurei.app/internal/uevent"
+)
+
+func TestMessage(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ v uevent.Message
+ want string
+ wantErr error
+ wantErrE error
+
+ s string
+ }{
+ {"sample virtio-sound-pci add", uevent.Message{
+ Action: uevent.KOBJ_ADD,
+ DevPath: "/devices/pci0000:00/0000:00:04.0/virtio1",
+ Env: []string{
+ "ACTION=add",
+ "DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1",
+ "SUBSYSTEM=virtio",
+ "MODALIAS=virtio:d00000019v00001AF4",
+ "SEQNUM=779",
+ },
+ }, "add@/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
+ "ACTION=add\x00" +
+ "DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
+ "SUBSYSTEM=virtio\x00" +
+ "MODALIAS=virtio:d00000019v00001AF4\x00" +
+ "SEQNUM=779\x00",
+ nil, nil, `add event on /devices/pci0000:00/0000:00:04.0/virtio1:
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1
+SUBSYSTEM=virtio
+MODALIAS=virtio:d00000019v00001AF4
+SEQNUM=779`},
+
+ {"sample virtio-sound-pci bind", uevent.Message{
+ Action: uevent.KOBJ_BIND,
+ DevPath: "/devices/pci0000:00/0000:00:04.0",
+ Env: []string{
+ "ACTION=bind",
+ "DEVPATH=/devices/pci0000:00/0000:00:04.0",
+ "SUBSYSTEM=pci",
+ "DRIVER=virtio-pci",
+ "PCI_CLASS=40100",
+ "PCI_ID=1AF4:1059",
+ "PCI_SUBSYS_ID=1AF4:1100",
+ "PCI_SLOT_NAME=0000:00:04.0",
+ "MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00",
+ "SEQNUM=780",
+ },
+ }, "bind@/devices/pci0000:00/0000:00:04.0\x00" +
+ "ACTION=bind\x00" +
+ "DEVPATH=/devices/pci0000:00/0000:00:04.0\x00" +
+ "SUBSYSTEM=pci\x00" +
+ "DRIVER=virtio-pci\x00" +
+ "PCI_CLASS=40100\x00" +
+ "PCI_ID=1AF4:1059\x00" +
+ "PCI_SUBSYS_ID=1AF4:1100\x00" +
+ "PCI_SLOT_NAME=0000:00:04.0\x00" +
+ "MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00\x00" +
+ "SEQNUM=780\x00", nil, nil, `bind event on /devices/pci0000:00/0000:00:04.0:
+ACTION=bind
+DEVPATH=/devices/pci0000:00/0000:00:04.0
+SUBSYSTEM=pci
+DRIVER=virtio-pci
+PCI_CLASS=40100
+PCI_ID=1AF4:1059
+PCI_SUBSYS_ID=1AF4:1100
+PCI_SLOT_NAME=0000:00:04.0
+MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00
+SEQNUM=780`},
+
+ {"zero devpath env", uevent.Message{
+ Action: uevent.KOBJ_MOVE,
+ }, "move@\x00", nil, nil, "move event:"},
+
+ {"d final NUL e bad action", uevent.Message{
+ Action: 0xbad,
+ }, "move@\x00truncated", &uevent.MessageError{
+ Data: "move@\x00truncated",
+ Section: "truncated",
+ Kind: uevent.MErrorKindFinalNUL,
+ }, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
+
+ {"bad action", uevent.Message{
+ Action: 0xbad,
+ }, "nonexistent@\x00", uevent.UnsupportedActionError(
+ "nonexistent",
+ ), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
+
+ {"d header sep e bad action", uevent.Message{
+ Action: 0xbad,
+ }, "move\x00", &uevent.MessageError{
+ Data: "move\x00",
+ Section: "move",
+ Kind: uevent.MErrorKindHeaderSep,
+ }, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
+
+ {"d missing header e bad action", uevent.Message{
+ Action: 0xbad,
+ }, "move", uevent.MissingHeaderError(
+ "move",
+ ), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ adeB(t, "", tc.v, tc.want, tc.wantErr, tc.wantErrE)
+ t.Run("string", func(t *testing.T) {
+ if got := tc.v.String(); got != tc.s {
+ t.Errorf("String: %q, want %q", got, tc.s)
+ }
+ })
+ })
+ }
+}
diff --git a/internal/uevent/uevent_test.go b/internal/uevent/uevent_test.go
index 9af3dcdd..53d9bafc 100644
--- a/internal/uevent/uevent_test.go
+++ b/internal/uevent/uevent_test.go
@@ -2,7 +2,6 @@ package uevent_test
import (
"encoding"
- "fmt"
"reflect"
"testing"
@@ -14,13 +13,14 @@ func adeT[V any, S interface {
encoding.TextAppender
encoding.TextMarshaler
encoding.TextUnmarshaler
- fmt.Stringer
*V
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
t.Helper()
- t.Run(name, func(t *testing.T) {
- t.Parallel()
+ f := func(t *testing.T) {
+ if name != "" {
+ t.Parallel()
+ }
t.Helper()
t.Run("decode", func(t *testing.T) {
@@ -49,15 +49,63 @@ func adeT[V any, S interface {
} else if err == nil && string(got) != want {
t.Errorf("MarshalText: %q, want %q", string(got), want)
}
- if wantErrE != nil {
+ })
+ }
+ if name != "" {
+ t.Run(name, f)
+ } else {
+ f(t)
+ }
+}
+
+// adeT sets up a binary subtest for a textual appender/decoder/encoder.
+func adeB[V any, S interface {
+ encoding.BinaryAppender
+ encoding.BinaryMarshaler
+ encoding.BinaryUnmarshaler
+
+ *V
+}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
+ t.Helper()
+ f := func(t *testing.T) {
+ if name != "" {
+ t.Parallel()
+ }
+ t.Helper()
+
+ t.Run("decode", func(t *testing.T) {
+ t.Parallel()
+ t.Helper()
+
+ var got V
+ if err := S(&got).UnmarshalBinary([]byte(want)); !reflect.DeepEqual(err, wantErr) {
+ t.Fatalf("UnmarshalBinary: error = %v, want %v", err, wantErr)
+ }
+ if wantErr != nil {
return
}
- if got := S(&v).String(); got != want {
- t.Errorf("String: %q, want %q", got, want)
+ if !reflect.DeepEqual(&got, &v) {
+ t.Errorf("UnmarshalBinary: %#v, want %#v", got, v)
}
})
- })
+
+ t.Run("encode", func(t *testing.T) {
+ t.Parallel()
+ t.Helper()
+
+ if got, err := S(&v).MarshalBinary(); !reflect.DeepEqual(err, wantErrE) {
+ t.Fatalf("MarshalBinary: error = %v, want %v", err, wantErrE)
+ } else if err == nil && string(got) != want {
+ t.Errorf("MarshalBinary: %q, want %q", string(got), want)
+ }
+ })
+ }
+ if name != "" {
+ t.Run(name, f)
+ } else {
+ f(t)
+ }
}
func TestErrors(t *testing.T) {
@@ -70,6 +118,26 @@ func TestErrors(t *testing.T) {
}{
{"UnsupportedActionError", uevent.UnsupportedActionError("explode"),
`unsupported kobject_action "explode"`},
+
+ {"MissingHeaderError", uevent.MissingHeaderError("move"),
+ `message "move" has no header`},
+
+ {"MessageError MErrorKindHeaderSep", &uevent.MessageError{
+ Data: "move\x00",
+ Section: "move",
+ Kind: uevent.MErrorKindHeaderSep,
+ }, `header "move" missing separator`},
+
+ {"MessageError MErrorKindFinalNUL", &uevent.MessageError{
+ Data: "move\x00truncated",
+ Section: "truncated",
+ Kind: uevent.MErrorKindFinalNUL,
+ }, `entry "truncated" missing NUL`},
+
+ {"MessageError bad", &uevent.MessageError{
+ Data: "\x00",
+ Kind: 0xbad,
+ }, `section "" is invalid`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {