ipc: close unauthenticated event-spoofing gap in emit's no-source path

The IPC "emit" method's no-source path took a bare event+data and sent it
straight to emit_tx tagged AdapterSource::System with zero validation of
the event name. Any same-UID process on the socket could send e.g.
{"event":"bread.power.ac.connected",...} and have it delivered to every
Lua subscriber indistinguishable from a real adapter event, since System
is the same tag the daemon uses for its own trusted, Rust-originated
sends (bread.system.startup, bread.profile.activated).

Fix, scoped to the actual threat model (same-UID Unix socket trust means
there's no way to cryptographically distinguish "the real bread-cli
binary" from any other local process, so a generic connection-identity
handshake would be theater):

- New AdapterSource::Manual tag for the no-source emit path. System is
  now reserved for daemon-internal, Rust-code-originated sends only and
  can never again be produced from data that arrived over the wire.
- The event name is rejected if its top-level dotted segment is one of
  the reserved, adapter-owned domains (RESERVED_DOMAINS in
  bread-shared/src/apps.rs) -- extended with bluetooth/workspace/window/
  monitor, event families the Hyprland and Bluetooth adapters already
  publish under but that were missing from that list. Freely-named
  custom/test event names are untouched, so `bread emit <name>` and
  bread-emit's fire-and-forget single-line-write design keep working
  exactly as documented.

Bumped API_VERSION to 1.5.0 and updated Documentation.md's IPC emit
section and Namespaces reserved-domains list accordingly.

Also fixed a subscribe/emit race that surfaced while adding regression
tests for this: events.subscribe's ack is written to the client before
the server task actually registers on the broadcast channel, so a test
that emits immediately after reading the ack can race the registration.
Added a settle delay plus an explicit timeout (instead of an unbounded
read loop) so a future regression fails the test instead of hanging the
whole binary.
This commit is contained in:
Breadway 2026-08-04 17:41:18 +08:00
parent 96639516b1
commit 7bb6fbb20f
6 changed files with 281 additions and 11 deletions

View file

@ -45,6 +45,19 @@ impl EventNormalizer {
source: raw.source.clone(),
data: raw.payload.clone(),
}],
// `Manual` is never constructed as a `RawEvent::source` in this
// codebase — the IPC boundary's unsourced `emit` path builds a
// `BreadEvent` directly (see `breadd/src/ipc/mod.rs`), bypassing
// this normalizer entirely, exactly as `System` did before it.
// This arm exists only so the match stays exhaustive; if that
// ever changes, pass-through (like `System`) is the sane default
// rather than silently dropping the event.
AdapterSource::Manual => vec![BreadEvent {
event: raw.kind.clone(),
timestamp: raw.timestamp,
source: raw.source.clone(),
data: raw.payload.clone(),
}],
};
out.retain(|ev| self.accept(ev));