// Package kobject interprets uevent messages from a NETLINK_KOBJECT_UEVENT socket. package kobject import ( "context" "fmt" "maps" "slices" "strconv" "sync" "hakurei.app/internal/report" "hakurei.app/internal/uevent" ) const ( // StateColdboot denotes an [Object] populated by a coldboot event. It is // eligible for all event actions. StateColdboot = iota // StateNew denotes an [Object] previously populated by a [uevent.KOBJ_ADD] // event, but has not yet been targeted by a [uevent.KOBJ_BIND] event, or // has been targeted by a [uevent.KOBJ_UNBIND] event. StateNew // StateBound denotes an [Object] that has been targeted by a // [uevent.KOBJ_BIND] and has not been targeted by a [uevent.KOBJ_UNBIND] // after that. StateBound ) // Object represents a kernel object. type Object struct { // Origin of the object. State int `json:"state,omitempty"` // Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE]. Offline bool `json:"offline,omitempty"` // alloc_uevent_skb: devpath DevPath string `json:"devpath"` // registered per-driver (optional) ModAlias string `json:"modalias,omitempty"` // dev_driver_uevent: drv->name (optional) Driver string `json:"driver,omitempty"` // SUBSYSTEM value set by the kernel. Subsystem string `json:"subsystem"` // Uninterpreted environment variable pairs. An entry missing a separator // gains the value "\x00". Env map[string]string `json:"env"` } // Clone returns the address of a copy of o. func (o *Object) Clone() *Object { v := *o v.Env = maps.Clone(o.Env) return &v } // GoString returns compound literal for the underlying value. func (o *Object) GoString() string { return fmt.Sprintf("&%#v", *o) } // merge merges uninterpreted environment variable pairs from an [Event]. func (o *Object) merge(env map[string]string) { for k, v := range env { if v == "\x00" { continue } switch k { case "MODALIAS": o.ModAlias = v continue case "DRIVER": o.Driver = v continue default: if o.Env == nil { o.Env = make(map[string]string) } o.Env[k] = v } } } // update updates o with pairs from env, optionally stripping visited pairs. func (o *Object) update(env map[string]string, strip bool) { for k := range o.Env { if v, ok := env[k]; ok { if strip { delete(env, k) } o.Env[k] = v } } } // A pendingIterator is a callback currently iterating through objects targeted // by ongoing events. type pendingIterator struct { f func(o *Object) bool done chan<- struct{} } // State processes a stream of [Event] populated from [uevent.Message] received // from a NETLINK_KOBJECT_UEVENT socket and presents an efficient representation // of kernel state. type State struct { // Next expected SEQNUM. seq uint64 // DevPath to environment variables. uevent map[string]*Object // Synchronises access to uevent and its objects. ueventMu sync.RWMutex // Alive iterators. iter []*pendingIterator // Synchronises access to iter. iterMu sync.Mutex // UUID for synthetic [uevent.Coldboot] events. coldboot uevent.UUID // Called on [uevent.KOBJ_CHANGE] with stripped environment variables. handleChange func(o *Object, env map[string]string) // Reports errors populating [Event] from [uevent.Message]. A user-supplied // nil value is replaced with a noop. reportErr func(error) } // New returns the address of a new [State]. func New( coldboot uevent.UUID, handleChange func(o *Object, env map[string]string), reportErr func(error), ) *State { return &State{ uevent: make(map[string]*Object), coldboot: coldboot, handleChange: handleChange, reportErr: reportErr, } } // deleteIter removes an iterator from s. Must be called after acquiring iterMu. func (s *State) deleteIter(p *pendingIterator) { s.iter = slices.DeleteFunc(s.iter, func(v *pendingIterator) bool { return p == v }) } // dispatchIter broadcasts an [Object] to all alive iterators. func (s *State) dispatchIter(o *Object) { s.iterMu.Lock() defer s.iterMu.Unlock() for _, p := range s.iter { if !p.f(o) { s.deleteIter(p) close(p.done) } } } // Range calls f on all current and upcoming [Object] values tracked by s until // f returns false or the context is cancelled. f must not retain o or modify // the value it points to. func (s *State) Range(ctx context.Context, f func(o *Object) bool) { done := make(chan struct{}) p := pendingIterator{f, done} s.iterMu.Lock() s.ueventMu.RLock() for _, o := range s.uevent { if !f(o) { s.ueventMu.RUnlock() s.iterMu.Unlock() return } } s.ueventMu.RUnlock() s.iter = append(s.iter, &p) s.iterMu.Unlock() select { case <-ctx.Done(): s.iterMu.Lock() s.deleteIter(&p) s.iterMu.Unlock() return case <-done: // deregistered by dispatchIter return } } // An EventError describes a malformed or inconsistent [Event]. type EventError struct { Kind int `json:"fault"` E Event `json:"event"` O *Object `json:"object,omitempty"` } var _ report.RepresentableError = EventError{} func (EventError) Representable() {} const ( // EUnexpectedColdboot is reported for a coldboot event with action other // than the expected [uevent.KOBJ_ADD]. EUnexpectedColdboot = iota // EDuplicateAdd is reported for a [uevent.KOBJ_ADD] event on a // still-existing entry that was not the result of a coldboot. EDuplicateAdd // EBadTarget is reported for an event on a nonexistent [Object]. This is // generally only possible before coldboot completes. EBadTarget // ERemoveState is reported for a [uevent.KOBJ_REMOVE] event targeting an // entry in a state other than [StateColdboot] and [StateNew]. ERemoveState // EUnexpectedOffline is reported for a [uevent.KOBJ_OFFLINE] or // [uevent.KOBJ_ONLINE] event targeting an already offline or online object. EUnexpectedOffline // EBindState is reported for a [uevent.KOBJ_BIND] event targeting an entry // in a state other than [StateColdboot] and [StateNew]. EBindState // EUnbindState is reported for a [uevent.KOBJ_UNBIND] event targeting an // entry in a state other than [StateBound]. EUnbindState // EMalformedMove is reported for a [uevent.KOBJ_MOVE] event missing the // DEVPATH_OLD environment variable. EMalformedMove ) func (e EventError) Error() string { switch e.Kind { case EUnexpectedColdboot: return "unexpected " + e.E.Action.String() + " coldboot event" case EDuplicateAdd: return "duplicate add event on devpath " + strconv.Quote(e.E.DevPath) case EBadTarget: return "unexpected " + e.E.Action.String() + " event on devpath " + strconv.Quote(e.E.DevPath) case ERemoveState: if e.O == nil { return "invalid remove event error" } return "remove event targeting devpath " + strconv.Quote(e.E.DevPath) + " in state " + strconv.Itoa(e.O.State) case EUnexpectedOffline: if e.O == nil { return "invalid unexpected offline error" } if e.O.Offline { return "offline event targeting devpath " + strconv.Quote(e.E.DevPath) } return "online event targeting devpath " + strconv.Quote(e.E.DevPath) case EBindState: if e.O == nil { return "invalid bind state error" } return "bind event targeting devpath " + strconv.Quote(e.E.DevPath) + " in state " + strconv.Itoa(e.O.State) case EUnbindState: if e.O == nil { return "invalid unbind state error" } return "unbind event targeting devpath " + strconv.Quote(e.E.DevPath) + " in state " + strconv.Itoa(e.O.State) case EMalformedMove: return "move event targeting devpath " + strconv.Quote(e.E.DevPath) + " missing DEVPATH_OLD" default: return "invalid event error kind " + strconv.Itoa(e.Kind) } } // NewError returns a new [EventError] for e and o. func (e *Event) NewError(kind int, o *Object) error { if o != nil { o = o.Clone() } return EventError{kind, e.Clone(), o} } // processEvent merges an event into s. func (s *State) processEvent(e *Event) { s.ueventMu.Lock() defer s.ueventMu.Unlock() coldboot := e.Synth != nil if e.Action != uevent.KOBJ_ADD && coldboot { s.reportErr(e.NewError(EUnexpectedColdboot, nil)) return } switch e.Action { case uevent.KOBJ_ADD: if e.Synth == nil { if o, ok := s.uevent[e.DevPath]; ok { s.reportErr(e.NewError(EDuplicateAdd, o)) o.merge(e.Env) s.dispatchIter(o) return } } o := e.makeColdboot() if !coldboot { o.State = StateNew } o.merge(e.Env) s.uevent[e.DevPath] = o s.dispatchIter(o) return case uevent.KOBJ_REMOVE: if o, ok := s.uevent[e.DevPath]; !ok { s.reportErr(e.NewError(EBadTarget, nil)) return } else if o.State != StateColdboot && o.State != StateNew { s.reportErr(e.NewError(ERemoveState, o)) } delete(s.uevent, e.DevPath) return case uevent.KOBJ_CHANGE: o, ok := s.uevent[e.DevPath] if !ok { s.reportErr(e.NewError(EBadTarget, nil)) // this suffers from the coldboot race window similar to KOBJ_MOVE, // however this action combines driver-specific and change-specific // environment variables and combines them with environment // variables meant to convey state of the kobject, and it is not // possible to reliably separate them, so this fallback avoids the // race at the cost of including some garbage in tracked state o = e.makeColdboot() o.merge(e.Env) s.uevent[e.DevPath] = o s.dispatchIter(o) return } o.update(e.Env, true) if s.handleChange != nil { s.handleChange(o, e.Env) } s.dispatchIter(o) return case uevent.KOBJ_MOVE: var o *Object if old, ok := e.Env["DEVPATH_OLD"]; !ok { s.reportErr(e.NewError(EMalformedMove, nil)) // not reached o = e.makeColdboot() } else if o, ok = s.uevent[old]; !ok { s.reportErr(e.NewError(EBadTarget, nil)) // this generally happens during coldboot, dropping the event here // may cause inconsistent state if the coldboot event for this // object was generated before the bind event delete(e.Env, "DEVPATH_OLD") o = e.makeColdboot() } else { delete(s.uevent, old) delete(e.Env, "DEVPATH_OLD") } o.merge(e.Env) s.uevent[e.DevPath] = o o.DevPath = e.DevPath s.dispatchIter(o) return case uevent.KOBJ_ONLINE: o, ok := s.uevent[e.DevPath] if !ok { s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE o = e.makeColdboot() s.uevent[e.DevPath] = o o.merge(e.Env) } if !o.Offline { s.reportErr(e.NewError(EUnexpectedOffline, o)) } o.Offline = false s.dispatchIter(o) return case uevent.KOBJ_OFFLINE: o, ok := s.uevent[e.DevPath] if !ok { s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE o = e.makeColdboot() s.uevent[e.DevPath] = o o.merge(e.Env) } if o.Offline { s.reportErr(e.NewError(EUnexpectedOffline, o)) } o.Offline = true s.dispatchIter(o) return case uevent.KOBJ_BIND: o, ok := s.uevent[e.DevPath] if !ok { s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE o = e.makeColdboot() s.uevent[e.DevPath] = o } if o.State != StateColdboot && o.State != StateNew { s.reportErr(e.NewError(EBindState, o)) } o.State = StateBound o.merge(e.Env) s.dispatchIter(o) return case uevent.KOBJ_UNBIND: o, ok := s.uevent[e.DevPath] if !ok { s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE, but does // not result in inconsistent state if dropped return } if o.State != StateBound { s.reportErr(e.NewError(EUnbindState, o)) } o.State = StateNew o.Driver = "" s.dispatchIter(o) return default: // not reached s.reportErr(fmt.Errorf("invalid action %d", e.Action)) return } } // BadSequenceError is reported for an unexpected SEQNUM. type BadSequenceError struct{ Got, Want uint64 } func (e BadSequenceError) Error() string { return "SEQNUM=" + strconv.FormatUint(e.Got, 10) + ", want " + strconv.FormatUint(e.Want, 10) } // Consume receives uevent messages and updates s to reflect state of kernel. func (s *State) Consume(ctx context.Context, events <-chan *uevent.Message) { if s.uevent == nil { s.uevent = make(map[string]*Object) } if s.reportErr == nil { s.reportErr = func(error) {} } var e Event for { select { case <-ctx.Done(): return case m, ok := <-events: if !ok { return } e.Populate(s.reportErr, m) // skip external synthetic event if e.Synth != nil && *e.Synth != s.coldboot { continue } if s.seq == 0 { s.seq = e.Sequence } if s.seq != e.Sequence { s.reportErr(BadSequenceError{e.Sequence, s.seq}) } s.seq++ s.processEvent(&e) } } }