aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/kobject/kobject.go
blob: 2acf3de034d2ecf0a83d9859a7b15a9bd61b8700 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
// 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, act uevent.KobjectAction) 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, act uevent.KobjectAction) {
	s.iterMu.Lock()
	defer s.iterMu.Unlock()

	for _, p := range s.iter {
		if !p.f(o, act) {
			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, act uevent.KobjectAction) bool,
) {
	done := make(chan struct{})
	p := pendingIterator{f, done}

	s.iterMu.Lock()
	s.ueventMu.RLock()
	for _, o := range s.uevent {
		if !f(o, uevent.KOBJ_ADD) {
			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 act := e.Action; act {
	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, act)
				return
			}
		}
		o := e.makeColdboot()
		if !coldboot {
			o.State = StateNew
		}
		o.merge(e.Env)
		s.uevent[e.DevPath] = o
		s.dispatchIter(o, act)
		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, act)
			return
		}
		o.update(e.Env, true)
		if s.handleChange != nil {
			s.handleChange(o, e.Env)
		}
		s.dispatchIter(o, act)
		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, act)
		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, act)
		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, act)
		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, act)
		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, act)
		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)
		}
	}
}