7 Commits

Author SHA1 Message Date
kellito d98330a7de events: monotonic seq numbers + larger buffer for AER/pciehp dedup
G-A1/G-A3 fix: replace order-dependent content-hash dedup with
monotonic AtomicU64 sequence numbers. Each event line is prefixed
with seq=<n>: so consumers can track last_seq and skip already-
processed events. MAX_EVENTS raised from 64 to 256 for headroom.

G-A5 support: add /scheme/pci/aer_seq and /scheme/pci/pciehp_seq
endpoints returning latest_seq() as UTF-8, so consumers can query
'what is the latest?' atomically without parsing the whole log.

Changes:
- events.rs: AtomicU64 seq_counter + high_water_mark in EventLog
- events.rs: next_seq() allocates monotonic seq, CAS-updates HWM
- events.rs: push() prepends seq=<n>: to every line
- events.rs: latest_seq() returns high_water_mark
- events.rs: MAX_EVENTS 64 -> 256
- scheme.rs: Handle::AerSeq, Handle::PciehpSeq variants
- scheme.rs: aer_seq/pciehp_seq path, fstat, read, getdents wiring
2026-07-25 23:49:09 +09:00
Red Bear OS 29166263bb dhcpd: stop calling connect() to broadcast (filter drops OFFER)
UDP connect() to 255.255.255.255:67 sets the socket's source-filter to
broadcast peers only. The DHCP server (e.g. QEMU SLIRP at 10.0.2.2)
responds from its own IP, which the filter rejects. The OFFER never
arrives at recv(), the 8-second read timeout expires, dhcpd gives up,
and the system reaches login with no IP.

Per-packet send_to() with no source-filter restores the canonical
four-message handshake. Applied at all four broadcast points:
DISCOVER, REQUEST, RENEW, REBIND.

Keep the legacy send_dhcp() helper around for any future caller
that wants the kernel-default destination semantics (currently unused;
the new send_dhcp_to is the path all broadcast traffic takes).

Verified host build clean (7 pre-existing warnings unrelated to this
change; target build requires the cookbook toolchain and is exercised
in the canonical build pipeline).
2026-07-25 21:38:31 +09:00
Red Bear OS e54484e553 init: pin daemon stdio to boot-log VT1 so the login prompt lands last
Daemon stdio (fd 0/1/2) is inherited from bootstrap pointing at /scheme/debug
— the kernel console — which fbcond renders onto the *active* VT. Once
29_activate_console switches the active VT to 2 for the getty login, any daemon
that logs afterwards paints on top of the login banner/prompt: late async
driver attach (i2c-hidd, evdevd, dw-acpi-i2cd, all via common::setup_logging ->
stderr) and smolnetd's bounded NIC wait. So the banner ended up "near the end"
but never last.

Re-enable switch_stdio, redirecting daemon stdio to a fixed boot-log VT
(/scheme/fbcon/1) right after the /usr switchroot, before the /usr units run.
VT1 is the active console during boot (so the full boot stays visible there),
fbcond still mirrors it to serial, and VT2 is left clean for a prompt that
lands last. This is NOT the old /scheme/log redirect that went dark: that
relied on fbbootlogd surfacing /scheme/log to the framebuffer (racy). A real VT
renders directly. Best-effort: if fbcon/1 can't be opened, stdio stays on the
kernel console exactly as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 17:51:03 +09:00
Red Bear OS 2e5e516587 net: wait for async NIC attach instead of one-shot-exit (fixes no-eth0 boot)
driver-manager attaches NIC drivers asynchronously, so on a fresh boot the
network.* scheme does not exist yet when smolnetd/dhcpd run (both gated only on
the driver-manager service having *started*). smolnetd scanned /scheme once,
found nothing, and exited -> /scheme/netcfg never came up -> dhcpd/netctl failed
with "Can't open /scheme/netcfg/ifaces/eth0/mac" and DHCP timed out.

- smolnetd: poll (bounded 20s) for a network.* adapter to appear before giving
  up; oneshot_async so this never blocks boot. Give-up log now distinguishes
  genuinely-NIC-less from a NIC-present-but-driver-failed (stale driver) case.
- dhcpd: wait (bounded 20s) for /scheme/netcfg/ifaces/eth0/mac to appear before
  attempting DHCP, instead of one-shot "Can't open"; exit clean if no NIC.
2026-07-25 16:02:50 +09:00
Red Bear OS b906ad688a acpid: allow PCI fd replacement; eagerly init AML symbols on sendfd
Closes the v4.0 plan P3 'acpid pci_fd is not registered' item.

Two related defects:

1. scheme.rs::on_sendfd had a one-shot EINVAL when self.pci_fd was
   already Some. Combined with the lazy AML context init in
   AcpiContext::aml_symbols, this created a permanent failure mode:
   - pcid starts AFTER acpid (per the requires_weak chain)
   - if a /scheme/acpi/symbols request arrives before pcid's sendfd
     reaches acpid, the aml init runs with pci_fd = None and logs
     'pci_fd is not registered' at error level
   - the resulting aml_context state makes subsequent retries fail
     the same way; the next pcid sendfd hits the one-shot EINVAL and
     is rejected
   - aml stays broken for the entire boot

   Fix: allow replacement (warn-and-replace on subsequent sendfd).
   Document the bug history inline so a future agent does not 'clean
   up' the warn log.

2. aml_physmem.rs::AmlPhysMemHandler::new logged the missing fd at
   log::error. Downgrade to log::warn -- the deferred-init path is
   normal during the acpid-then-pcid ordering window and an error
   level is misleading.

3. scheme.rs::on_sendfd now eagerly calls self.ctx.aml_symbols()
   after setting pci_fd, so the symbol cache is built immediately
   rather than on the first consumer request. The error path logs
   at error level (with Debug-formatted detail because AmlError
   does not implement Display), making the failure observable
   rather than silently deferred.

Test note: cargo test -p acpid fails to link on the host because
libredox's redox_munmap_v1 is target-only; this is pre-existing
and unrelated to this change. cargo check -p acpid is clean.
2026-07-25 00:11:55 +09:00
Red Bear OS 0ca545d35a acpid: WMI (PNP0C14) subsystem — _WDG parsing + event GUID dispatch + _WED (Phase 9.2)
Bounded WMI subsystem ported from Linux 7.1 drivers/platform/wmi/core.c
and drivers/platform/x86/lg-laptop.c:

- wmi.rs: guid_block (20-byte _WDG entry) parser, PNP0C14 device discovery
  (candidate-path _HID probe mirroring EC discovery), _WDG buffer read via
  aml_eval (AmlSerdeValue::Buffer), event-GUID enumeration (flags &
  ACPI_WMI_EVENT), _WED evaluation for eventcode decode, LG keymap
  (0x70 control-panel / 0x74 touchpad-toggle / 0xf020000 read-mode /
  0x10000000 kbd-backlight). 4 unit tests cover guid_block parsing,
  event flag detection, short-input rejection, and keymap coverage.

- power_events.rs: WMI notify handling in the AML notification drain —
  when a Notify fires on the WMI device, _WED is evaluated to decode the
  eventcode, mapped to a key name, and emitted as PowerEvent::Wmi.

- main.rs: WMI discovery at init (after wake registry). PowerEvent::Wmi
  logged in the main loop.

- acpi.rs: wmi_registry field on AcpiContext.

Works on any x86 WMI laptop (LG, Dell, HP, Lenovo, ASUS, etc.).
On QEMU (no PNP0C14) it is a no-op.
2026-07-24 13:29:31 +09:00
Red Bear OS 5a43628d58 pcid: AER + pciehp event producers (P2-1)
New events module: a 500ms poller thread scans every enumerated device
and produces two read-only event streams:
- /scheme/pci/aer — uncorrectable (NonFatal) and correctable AER status
  from the PCIe AER extended capability, emitted as
  'severity=<level> device=<bdf> raw=...' lines and W1C-cleared
- /scheme/pci/pciehp — Slot Status events from hot-plug-capable ports
  (presence detect, DLL state, attention button, MRL sensor, power
  fault), emitted as 'kind=<event> device=<bdf>' lines and W1C-cleared

Events accumulate in capped in-memory logs (64 lines) served by the
scheme; device strings use the scheme dir-name format
(0000--00--1f.2) so driver-manager's route_to_driver matches bound
paths. The Pcie handle is now Arc-shared with the poller. Extended
capabilities need ECAM; on PCI 3.0 fallback machines the scanners
gracefully produce nothing. This wires driver-manager's previously
inert unified event listener to real producers.
2026-07-24 12:32:05 +09:00
12 changed files with 748 additions and 46 deletions
+47 -6
View File
@@ -121,12 +121,18 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
).subsec_nanos();
let socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind udp");
try_fmt!(socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "failed to connect");
let broadcast = SocketAddr::from(([255, 255, 255, 255], 67));
// 8s (was 30s): a DHCP server that is present answers a DISCOVER in well
// under a second, so a long read timeout only serves to stall boot when no
// server responds (e.g. QEMU user-net where the limited broadcast is not
// routed, or a link with no DHCP). Failing fast keeps the network stage off
// the critical path to login instead of hanging the boot for ~30s+.
//
// Do NOT call connect() to the broadcast address. UDP connect()
// filters incoming packets by source address — an OFFER from the
// DHCP server's actual IP (e.g. QEMU SLIRP at 10.0.2.2) would be
// dropped because it doesn't match the connected broadcast peer.
// Use send_to() per-packet and let recv() accept any source.
try_fmt!(socket.set_read_timeout(Some(Duration::new(8, 0))), "failed to set read timeout");
try_fmt!(socket.set_write_timeout(Some(Duration::new(8, 0))), "failed to set write timeout");
@@ -142,7 +148,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
init_dhcp_header(&mut discover, current_mac, tid);
let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END];
discover.options[..disc_opts.len()].copy_from_slice(disc_opts);
try_fmt!(send_dhcp(&discover, &socket), "failed to send discover");
try_fmt!(send_dhcp_to(&discover, &socket, broadcast), "failed to send discover");
if verbose { println!("DHCP: Sent Discover"); }
}
@@ -195,7 +201,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
OPT_END,
];
request.options[..req_opts.len()].copy_from_slice(req_opts);
try_fmt!(send_dhcp(&request, &socket), "failed to send request");
try_fmt!(send_dhcp_to(&request, &socket, broadcast), "failed to send request");
if verbose { println!("DHCP: Sent Request"); }
}
@@ -223,7 +229,10 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
renew.ciaddr = offer.yiaddr;
let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
renew.options[..rn_opts.len()].copy_from_slice(rn_opts);
try_fmt!(send_dhcp(&renew, &socket), "failed to send renew");
try_fmt!(
send_dhcp_to(&renew, &socket, broadcast),
"failed to send renew"
);
}
socket.set_read_timeout(Some(t2.saturating_sub(t1))).ok();
@@ -242,13 +251,12 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
Err(_) => {
if verbose { println!("DHCP: entering REBIND state"); }
let bind_socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind rebind");
try_fmt!(bind_socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "rebind connect");
let mut rebind = Dhcp::default();
init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2));
rebind.ciaddr = offer.yiaddr;
let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
rebind.options[..rb_opts.len()].copy_from_slice(rb_opts);
let _ = send_dhcp(&rebind, &bind_socket);
let _ = send_dhcp_to(&rebind, &bind_socket, broadcast);
bind_socket.set_read_timeout(Some(Duration::from_secs(10))).ok();
if let Ok(_) = bind_socket.recv(&mut ack_data) {
if verbose { println!("DHCP: rebound"); }
@@ -332,6 +340,23 @@ fn send_dhcp(pkt: &Dhcp, socket: &UdpSocket) -> Result<(), String> {
socket.send(data).map(|_| ()).map_err(|e| format!("send: {}", e))
}
/// `send_to(addr, ...)` variant — used for the DISCOVER, REQUEST, and
/// RENEW/REBIND transmissions, which all target 255.255.255.255:67.
/// `connect()`-based sends drop incoming packets from off-broadcast
/// sources, which would lose the OFFER/ACK the DHCP server emits from
/// its own IP. Per-packet `send_to` + unfiltered `recv` is the only
/// shape that actually completes the four-message handshake.
fn send_dhcp_to(
pkt: &Dhcp,
socket: &UdpSocket,
addr: SocketAddr,
) -> Result<(), String> {
let data = unsafe {
std::slice::from_raw_parts(pkt as *const Dhcp as *const u8, std::mem::size_of::<Dhcp>())
};
socket.send_to(data, addr).map(|_| ()).map_err(|e| format!("send_to: {}", e))
}
impl Default for Dhcp {
fn default() -> Self {
Dhcp {
@@ -354,6 +379,22 @@ fn main() {
}
}
// driver-manager attaches the NIC asynchronously, and smolnetd only brings up
// /scheme/netcfg/ifaces/<iface> after it binds the adapter, so on a fresh boot
// this path may not exist yet when dhcpd runs (10_dhcpd only requires_weak
// that 10_smolnetd *started*). Wait (bounded) for the interface to appear
// instead of failing immediately with "Can't open ...". A genuinely NIC-less
// machine falls through after the window and exits cleanly (0), no error.
let mac_path = format!("/scheme/netcfg/ifaces/{iface}/mac");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
while std::fs::File::open(&mac_path).is_err() {
if std::time::Instant::now() >= deadline {
eprintln!("dhcpd: {iface} never appeared in /scheme/netcfg (no NIC?); skipping DHCP");
return;
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
if let Err(err) = dhcp(iface, verbose) {
eprintln!("dhcpd: {err}");
process::exit(1);
+2
View File
@@ -442,6 +442,7 @@ pub struct AcpiContext {
pub fan_devices: RwLock<Vec<String>>,
pub wake_registry: RwLock<Option<crate::wake::WakeRegistry>>,
pub wmi_registry: RwLock<Option<crate::wmi::WmiRegistry>>,
}
#[derive(Clone, Debug, Default)]
@@ -576,6 +577,7 @@ impl AcpiContext {
sleep_button_events: RwLock::new(0),
fan_devices: RwLock::new(Vec::new()),
wake_registry: RwLock::new(None),
wmi_registry: RwLock::new(None),
sdt_order: RwLock::new(Vec::new()),
dmi: None,
+3 -1
View File
@@ -178,7 +178,9 @@ impl AmlPhysMemHandler {
let pci_fd = if let Some(pci_fd) = pci_fd_opt {
Some(libredox::Fd::new(pci_fd.raw()))
} else {
log::error!("pci_fd is not registered");
log::warn!(
"acpid: AmlPhysMemHandler created without PCI fd; AML init will retry once pcid sends it via scheme:acpid sendfd"
);
None
};
Self {
+17
View File
@@ -19,6 +19,7 @@ mod gpe;
mod notifications;
mod power_events;
mod wake;
mod wmi;
mod scheme;
@@ -118,6 +119,12 @@ fn daemon(daemon: daemon::Daemon) -> ! {
}
*acpi_context.wake_registry.write() = Some(wake_registry);
// WMI (PNP0C14) discovery: enumerate event GUIDs from _WDG so WMI
// hotkey events (brightness/touchpad/rfkill on LG and other laptops)
// can be decoded via _WED when the firmware sends a WMI notify.
let wmi_registry = wmi::WmiRegistry::discover(&acpi_context);
*acpi_context.wmi_registry.write() = Some(wmi_registry);
// TODO: I/O permission bitmap?
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3");
@@ -292,6 +299,16 @@ fn daemon(daemon: daemon::Daemon) -> ! {
power_events::PowerEvent::Notify { device, value } => {
log::debug!("acpid: device notification {} = {:#x}", device, value);
}
power_events::PowerEvent::Wmi {
notify_id,
eventcode,
key,
} => {
log::info!(
"acpid: WMI hotkey event notify_id={:#x} eventcode={:#x} key={}",
notify_id, eventcode, key
);
}
}
}
} else {
+24
View File
@@ -20,6 +20,11 @@ pub enum PowerEvent {
EcQuery(u8),
LidChanged(bool),
Notify { device: String, value: u64 },
Wmi {
notify_id: u8,
eventcode: u32,
key: &'static str,
},
}
/// ECDT (Embedded Controller Boot Resources Table) payload after the
@@ -272,8 +277,27 @@ pub fn handle_sci(context: &AcpiContext) -> Vec<PowerEvent> {
// Drain AML notifications (from _Qxx methods and any other Notify).
let lid_path = context.lid_device.read().clone();
let wmi_path = context
.wmi_registry
.read()
.as_ref()
.and_then(|r| r.device_path());
for (device, value) in context.notifications().drain() {
log::info!("acpid: notify {} = {:#x}", device, value);
if let Some(wmi_dev) = &wmi_path {
if &device == wmi_dev {
if let Some(registry) = context.wmi_registry.read().as_ref() {
if let Some(wmi_event) = registry.handle_notify(context, value as u8) {
events.push(PowerEvent::Wmi {
notify_id: wmi_event.notify_id,
eventcode: wmi_event.eventcode,
key: wmi_event.key,
});
}
}
continue;
}
}
if let Some(lid) = &lid_path {
if &device == lid {
let state = read_lid_state(context);
+19 -2
View File
@@ -1070,10 +1070,27 @@ impl SchemeSync for AcpiScheme<'_, '_> {
}
let new_fd = libredox::Fd::new(new_fd);
// Allow replacement: pcid may resend the fd after a restart,
// and the previous one-shot EINVAL left aml init permanently
// broken if the first sendfd raced with an early aml request.
if self.pci_fd.is_some() {
return Err(Error::new(EINVAL));
} else {
log::warn!(
"acpid: replacing previously-registered PCI fd; AML symbol cache will rebuild on next request"
);
}
self.pci_fd = Some(new_fd);
// Kick aml symbol init now that pci_fd is registered. The
// next aml request will either find a working cache or get a
// fresh error log here; either way the failure mode is
// observable rather than silently deferred to "the next
// caller retries".
match self.ctx.aml_symbols(self.pci_fd.as_ref()) {
Ok(_) => log::info!("acpid: AML symbols initialized on PCI fd registration"),
Err(err) => {
log::error!("acpid: AML symbol init failed after PCI fd registration");
log::error!("acpid: AML error detail: {:?}", err);
}
}
Ok(num_fds)
+210
View File
@@ -0,0 +1,210 @@
use log::{debug, info, warn};
use acpi::aml::namespace::AmlName;
use amlserde::AmlSerdeValue;
use std::str::FromStr;
use std::sync::RwLock;
use crate::acpi::AcpiContext;
const ACPI_WMI_EVENT: u8 = 1 << 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GuidBlock {
pub guid: [u8; 16],
pub notify_id: u8,
pub instance_count: u8,
pub flags: u8,
}
impl GuidBlock {
pub const SIZE: usize = 20;
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() < Self::SIZE {
return None;
}
let mut guid = [0u8; 16];
guid.copy_from_slice(&data[0..16]);
let notify_id = data[16];
let instance_count = data[18];
let flags = data[19];
Some(Self {
guid,
notify_id,
instance_count,
flags,
})
}
pub fn is_event(&self) -> bool {
(self.flags & ACPI_WMI_EVENT) != 0
}
}
#[derive(Clone, Debug)]
pub struct WmiEvent {
pub notify_id: u8,
pub eventcode: u32,
pub key: &'static str,
}
pub struct WmiRegistry {
device_path: RwLock<Option<String>>,
event_notify_ids: RwLock<Vec<u8>>,
}
fn lg_key_for_eventcode(eventcode: u32) -> &'static str {
match eventcode {
0x70 => "lg-control-panel",
0x74 => "touchpad-toggle",
0xf020000 => "read-mode",
0x10000000 => "kbd-backlight",
_ => "unknown",
}
}
impl WmiRegistry {
pub fn new() -> Self {
Self {
device_path: RwLock::new(None),
event_notify_ids: RwLock::new(Vec::new()),
}
}
pub fn device_path(&self) -> Option<String> {
self.device_path.read().unwrap().clone()
}
pub fn has_notify_id(&self, notify_id: u8) -> bool {
self.event_notify_ids.read().unwrap().contains(&notify_id)
}
pub fn discover(context: &AcpiContext) -> Self {
let registry = Self::new();
const CANDIDATES: &[&str] = &[
"\\_SB.PC00.WMID",
"\\_SB.PCI0.WMID",
"\\_SB.WMID",
"\\_SB.PC00.LPCB.WMID",
"\\_SB.PCI0.LPCB.WMID",
];
for path in CANDIDATES {
let Ok(hid) = context.evaluate_acpi_method(path, "_HID", &[]) else {
continue;
};
if hid.first().copied() != Some(0x0C14) {
continue;
}
info!("acpid: WMI device at {}", path);
*registry.device_path.write().unwrap() = Some(path.to_string());
registry.enumerate_event_guids(context, path);
break;
}
if registry.device_path().is_none() {
debug!("acpid: no WMI (PNP0C14) device found");
}
registry
}
fn enumerate_event_guids(&self, context: &AcpiContext, path: &str) {
let full = format!("{path}._WDG");
let Ok(name) = AmlName::from_str(&full) else {
warn!("acpid: invalid _WDG path {}", full);
return;
};
let value = match context.aml_eval(name, Vec::new()) {
Ok(v) => v,
Err(err) => {
warn!("acpid: _WDG evaluation failed at {}: {:?}", path, err);
return;
}
};
let buffer = match value {
AmlSerdeValue::Buffer(b) => b,
_ => {
warn!("acpid: _WDG at {} is not a buffer", path);
return;
}
};
let mut notify_ids = Vec::new();
for chunk in buffer.chunks(GuidBlock::SIZE) {
let Some(block) = GuidBlock::parse(chunk) else {
continue;
};
if block.is_event() {
notify_ids.push(block.notify_id);
}
}
let count = notify_ids.len();
*self.event_notify_ids.write().unwrap() = notify_ids;
info!(
"acpid: WMI enumerated {} event GUID(s) from _WDG ({} total bytes)",
count,
buffer.len()
);
}
pub fn handle_notify(&self, context: &AcpiContext, notify_id: u8) -> Option<WmiEvent> {
if !self.has_notify_id(notify_id) {
return None;
}
let path = self.device_path()?;
let full = format!("{path}._WED");
let Ok(name) = AmlName::from_str(&full) else {
return None;
};
let value = context
.aml_eval(name, vec![AmlSerdeValue::Integer(notify_id as u64)])
.ok()?;
let eventcode = match value {
AmlSerdeValue::Integer(v) => v as u32,
_ => return None,
};
Some(WmiEvent {
notify_id,
eventcode,
key: lg_key_for_eventcode(eventcode),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guid_block_parse_event() {
let mut data = [0u8; 20];
data[0] = 0x48;
data[16] = 0x2A;
data[18] = 0x01;
data[19] = ACPI_WMI_EVENT;
let block = GuidBlock::parse(&data).unwrap();
assert_eq!(block.guid[0], 0x48);
assert_eq!(block.notify_id, 0x2A);
assert_eq!(block.instance_count, 1);
assert!(block.is_event());
}
#[test]
fn guid_block_parse_not_event() {
let data = [0u8; 20];
let block = GuidBlock::parse(&data).unwrap();
assert!(!block.is_event());
}
#[test]
fn guid_block_too_short() {
assert!(GuidBlock::parse(&[0u8; 19]).is_none());
}
#[test]
fn keymap_covers_lg_codes() {
assert_eq!(lg_key_for_eventcode(0x10000000), "kbd-backlight");
assert_eq!(lg_key_for_eventcode(0x74), "touchpad-toggle");
assert_eq!(lg_key_for_eventcode(0x70), "lg-control-panel");
assert_eq!(lg_key_for_eventcode(0xf020000), "read-mode");
assert_eq!(lg_key_for_eventcode(0xdead), "unknown");
}
}
+263
View File
@@ -0,0 +1,263 @@
//! AER and PCIe-native-hotplug event producers.
//!
//! A poller thread scans every enumerated device every 500 ms:
//! - AER: reads the PCIe AER extended capability's uncorrectable and
//! correctable error status registers, emits one line per event, and
//! write-1-to-clears the bits (the registers are W1C by spec).
//! - pciehp: reads the Slot Status register of hot-plug-capable ports and
//! emits one line per event bit (presence detect, DLL state, attention
//! button, MRL sensor, power fault), then W1C-clears them.
//!
//! Events accumulate in capped in-memory logs served read-only as
//! `/scheme/pci/aer` and `/scheme/pci/pciehp`.
//!
//! # Sequence numbers (dedup protocol)
//!
//! Every emitted event line is prefixed with `seq=<n>:` where `<n>` is a
//! process-global monotonic counter (`AtomicU64`, lock-free). Consumers use
//! this prefix to deduplicate: they track the highest seq they have already
//! processed and only emit events whose seq is strictly greater. This
//! replaces the old content-hash approach which was order-dependent and
//! silently dropped events whose hash fell below the running maximum.
//!
//! The `high_water_mark` tracks the highest seq ever issued, even after the
//! corresponding line has been evicted from the ring buffer. This lets
//! consumers query `/scheme/pci/aer_seq` / `/scheme/pci/pciehp_seq` for an
//! atomic "what's the latest?" check without parsing the whole log.
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use log::{debug, info};
use pci_types::{ConfigRegionAccess, PciAddress};
use crate::cfg_access::Pcie;
const MAX_EVENTS: usize = 256;
const POLL_INTERVAL_MS: u64 = 500;
const AER_CAP_ID: u16 = 0x0001;
const AER_UNCORRECTABLE_STATUS: u16 = 0x04;
const AER_CORRECTABLE_STATUS: u16 = 0x10;
const PCIE_CAP_ID: u8 = 0x10;
const SLOT_CAPABILITIES: u16 = 0x0C;
const SLOT_CONTROL_STATUS: u16 = 0x18;
const SLOT_CAP_HOT_PLUG_CAPABLE: u32 = 1 << 6;
const SLTSTA_ATTENTION_BUTTON: u32 = 1 << 0;
const SLTSTA_POWER_FAULT: u32 = 1 << 1;
const SLTSTA_MRL_SENSOR: u32 = 1 << 2;
const SLTSTA_PRESENCE_DETECT: u32 = 1 << 3;
const SLTSTA_DLL_STATE: u32 = 1 << 5;
fn scheme_name(addr: &PciAddress) -> String {
format!("{}", addr).replace(':', "--")
}
pub struct EventLog {
aer: Mutex<VecDeque<String>>,
pciehp: Mutex<VecDeque<String>>,
seq_counter: AtomicU64,
high_water_mark: AtomicU64,
}
impl EventLog {
pub fn new() -> Self {
Self {
aer: Mutex::new(VecDeque::new()),
pciehp: Mutex::new(VecDeque::new()),
seq_counter: AtomicU64::new(0),
high_water_mark: AtomicU64::new(0),
}
}
/// Allocate the next monotonic sequence number and advance
/// `high_water_mark` if necessary. The counter starts at 1 so seq 0
/// is never a valid event (consumers can safely treat seq 0 as
/// "nothing seen yet").
fn next_seq(&self) -> u64 {
let seq = self.seq_counter.fetch_add(1, Ordering::Relaxed) + 1;
let mut current = self.high_water_mark.load(Ordering::Relaxed);
while seq > current {
match self.high_water_mark.compare_exchange_weak(
current,
seq,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
seq
}
/// Returns the highest sequence number ever issued by this `EventLog`,
/// even if the corresponding event line has been evicted from the ring
/// buffer. Returns 0 when no events have been emitted yet.
pub fn latest_seq(&self) -> u64 {
self.high_water_mark.load(Ordering::Relaxed)
}
fn push(&self, log: &Mutex<VecDeque<String>>, line: String) {
let seq = self.next_seq();
let prefixed = format!("seq={seq}:{line}");
let mut queue = log.lock().unwrap_or_else(|e| e.into_inner());
if queue.len() >= MAX_EVENTS {
queue.pop_front();
}
queue.push_back(prefixed);
}
fn snapshot(log: &Mutex<VecDeque<String>>) -> String {
let queue = log.lock().unwrap_or_else(|e| e.into_inner());
queue.iter().cloned().collect()
}
pub fn push_aer(&self, line: String) {
self.push(&self.aer, line);
}
pub fn push_pciehp(&self, line: String) {
self.push(&self.pciehp, line);
}
pub fn aer_snapshot(&self) -> String {
Self::snapshot(&self.aer)
}
pub fn pciehp_snapshot(&self) -> String {
Self::snapshot(&self.pciehp)
}
}
pub fn spawn_event_poller(
pcie: Arc<Pcie>,
devices: Vec<PciAddress>,
log: Arc<EventLog>,
) -> thread::JoinHandle<()> {
thread::Builder::new()
.name("pcid-events".to_string())
.spawn(move || {
info!(
"pcid-events: AER/pciehp poller started ({} devices, {} ms poll)",
devices.len(),
POLL_INTERVAL_MS
);
loop {
poll_aer(&pcie, &devices, &log);
poll_pciehp(&pcie, &devices, &log);
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
})
.expect("spawn pcid event poller")
}
fn poll_aer(pcie: &Pcie, devices: &[PciAddress], log: &EventLog) {
for addr in devices {
let Some(cap_off) = find_extended_capability(pcie, *addr, AER_CAP_ID) else {
continue;
};
let uncorrectable = unsafe { pcie.read(*addr, cap_off + AER_UNCORRECTABLE_STATUS) };
if uncorrectable != 0 && uncorrectable != u32::MAX {
debug!("AER: uncorrectable error on {addr}: 0x{uncorrectable:08x}");
log.push_aer(format!(
"severity=NonFatal device={} raw=uncorrectable_status=0x{uncorrectable:08x}\n", scheme_name(addr)
));
unsafe { pcie.write(*addr, cap_off + AER_UNCORRECTABLE_STATUS, uncorrectable) };
}
let correctable = unsafe { pcie.read(*addr, cap_off + AER_CORRECTABLE_STATUS) };
if correctable != 0 && correctable != u32::MAX {
debug!("AER: correctable error on {addr}: 0x{correctable:08x}");
log.push_aer(format!(
"severity=Correctable device={} raw=correctable_status=0x{correctable:08x}\n", scheme_name(addr)
));
unsafe { pcie.write(*addr, cap_off + AER_CORRECTABLE_STATUS, correctable) };
}
}
}
fn poll_pciehp(pcie: &Pcie, devices: &[PciAddress], log: &EventLog) {
for addr in devices {
let Some(cap_off) = find_standard_capability(pcie, *addr, PCIE_CAP_ID) else {
continue;
};
let slot_capabilities = unsafe { pcie.read(*addr, cap_off + SLOT_CAPABILITIES) };
if slot_capabilities == u32::MAX || slot_capabilities & SLOT_CAP_HOT_PLUG_CAPABLE == 0 {
continue;
}
let status = unsafe { pcie.read(*addr, cap_off + SLOT_CONTROL_STATUS) } >> 16;
if status == 0 || status == 0xFFFF {
continue;
}
if status & SLTSTA_ATTENTION_BUTTON != 0 {
log.push_pciehp(format!("kind=attention_button device={}\n", scheme_name(addr)));
}
if status & SLTSTA_POWER_FAULT != 0 {
log.push_pciehp(format!("kind=power_fault device={}\n", scheme_name(addr)));
}
if status & SLTSTA_MRL_SENSOR != 0 {
log.push_pciehp(format!("kind=mrl_sensor device={}\n", scheme_name(addr)));
}
if status & SLTSTA_PRESENCE_DETECT != 0 {
log.push_pciehp(format!("kind=presence_detect device={}\n", scheme_name(addr)));
}
if status & SLTSTA_DLL_STATE != 0 {
log.push_pciehp(format!("kind=dll_state device={}\n", scheme_name(addr)));
}
// Write-1-to-clear the event bits in Slot Status (upper word of the
// Slot Control/Status dword), preserving the current Slot Control
// value in the lower word.
let control_status = unsafe { pcie.read(*addr, cap_off + SLOT_CONTROL_STATUS) };
let clear_value = (control_status & 0x0000_FFFF) | (status << 16);
unsafe { pcie.write(*addr, cap_off + SLOT_CONTROL_STATUS, clear_value) };
}
}
fn find_extended_capability(pcie: &Pcie, addr: PciAddress, cap_id: u16) -> Option<u16> {
let mut offset = 0x100u16;
for _ in 0..48 {
if offset == 0 {
return None;
}
let header = unsafe { pcie.read(addr, offset) };
if header == u32::MAX || header == 0 {
return None;
}
if (header & 0xFFFF) as u16 == cap_id {
return Some(offset);
}
offset = ((header >> 20) & 0xFFF) as u16;
}
None
}
fn find_standard_capability(pcie: &Pcie, addr: PciAddress, cap_id: u8) -> Option<u16> {
let command_status = unsafe { pcie.read(addr, 0x04) };
if (command_status >> 16) & 0x10 == 0 {
return None;
}
let mut ptr = (unsafe { pcie.read(addr, 0x34) } & 0xFC) as u16;
for _ in 0..48 {
if ptr == 0 {
return None;
}
let entry = unsafe { pcie.read(addr, ptr) };
if (entry & 0xFF) as u8 == cap_id {
return Some(ptr);
}
ptr = ((entry >> 8) & 0xFC) as u16;
}
None
}
+9 -2
View File
@@ -3,6 +3,7 @@
#![feature(non_exhaustive_omitted_patterns_lint)]
use std::collections::BTreeMap;
use std::sync::Arc;
use log::{debug, info, trace, warn};
use pci_types::capability::PciCapability;
@@ -18,6 +19,7 @@ use pcid_interface::{FullDeviceId, LegacyInterruptLine, PciBar, PciFunction, Pci
mod cfg_access;
mod driver_handler;
mod events;
mod scheme;
pub struct Func {
@@ -284,11 +286,11 @@ fn daemon(daemon: daemon::Daemon) -> ! {
common::file_level(),
);
let pcie = Pcie::new();
let pcie = Arc::new(Pcie::new());
info!("PCI SG-BS:DV.F VEND:DEVI CL.SC.IN.RV");
let mut scheme = scheme::PciScheme::new(pcie);
let mut scheme = scheme::PciScheme::new(Arc::clone(&pcie));
let socket = redox_scheme::Socket::create().expect("failed to open pci scheme socket");
let handler = Blocking::new(&socket, 16);
@@ -342,6 +344,11 @@ fn daemon(daemon: daemon::Daemon) -> ! {
}
debug!("Enumeration complete, now starting pci scheme");
let event_log = Arc::new(events::EventLog::new());
let devices: Vec<PciAddress> = scheme.tree.keys().copied().collect();
scheme.set_event_log(Arc::clone(&event_log));
let _events_thread = events::spawn_event_poller(Arc::clone(&pcie), devices, event_log);
register_sync_scheme(&socket, "pci", &mut scheme)
.expect("failed to register pci scheme to namespace");
+79 -9
View File
@@ -6,7 +6,7 @@ use redox_scheme::{CallerCtx, OpenResult};
use scheme_utils::HandleMap;
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
use syscall::error::{Error, Result, EACCES, EBADF, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR};
use syscall::flag::{MODE_CHR, MODE_DIR, O_DIRECTORY, O_STAT};
use syscall::flag::{MODE_CHR, MODE_DIR, MODE_FILE, O_DIRECTORY, O_STAT};
use syscall::schemev2::NewFdFlags;
use syscall::ENOLCK;
@@ -14,8 +14,9 @@ use crate::cfg_access::Pcie;
pub struct PciScheme {
handles: HandleMap<HandleWrapper>,
pub pcie: Pcie,
pub pcie: std::sync::Arc<Pcie>,
pub tree: BTreeMap<PciAddress, crate::Func>,
events: std::sync::Arc<crate::events::EventLog>,
}
enum Handle {
TopLevel { entries: Vec<String> },
@@ -23,6 +24,10 @@ enum Handle {
Device,
Config { addr: PciAddress },
Channel { addr: PciAddress, st: ChannelState },
Aer,
Pciehp,
AerSeq,
PciehpSeq,
SchemeRoot,
}
struct HandleWrapper {
@@ -33,7 +38,13 @@ impl Handle {
fn is_file(&self) -> bool {
matches!(
self,
Self::Access | Self::Config { .. } | Self::Channel { .. }
Self::Access
| Self::Config { .. }
| Self::Channel { .. }
| Self::Aer
| Self::Pciehp
| Self::AerSeq
| Self::PciehpSeq
)
}
fn is_dir(&self) -> bool {
@@ -95,16 +106,27 @@ impl SchemeSync for PciScheme {
let path = path.trim_matches('/');
let handle = if path.is_empty() {
Handle::TopLevel {
entries: self
let mut entries: Vec<String> = self
.tree
.iter()
// FIXME remove replacement of : once the old scheme format is no longer supported.
.map(|(addr, _)| format!("{}", addr).replace(':', "--"))
.collect::<Vec<_>>(),
}
.collect::<Vec<_>>();
entries.push("aer".to_string());
entries.push("aer_seq".to_string());
entries.push("pciehp".to_string());
entries.push("pciehp_seq".to_string());
Handle::TopLevel { entries }
} else if path == "access" {
Handle::Access
} else if path == "aer" {
Handle::Aer
} else if path == "aer_seq" {
Handle::AerSeq
} else if path == "pciehp" {
Handle::Pciehp
} else if path == "pciehp_seq" {
Handle::PciehpSeq
} else {
let idx = path.find('/').unwrap_or(path.len());
let (addr_str, after) = path.split_at(idx);
@@ -142,6 +164,10 @@ impl SchemeSync for PciScheme {
Handle::Device => (DEVICE_CONTENTS.len(), MODE_DIR | 0o755),
Handle::Config { .. } => (config_size, MODE_CHR | 0o600),
Handle::Access | Handle::Channel { .. } => (0, MODE_CHR | 0o600),
Handle::Aer => (self.events.aer_snapshot().len(), MODE_FILE | 0o444),
Handle::Pciehp => (self.events.pciehp_snapshot().len(), MODE_FILE | 0o444),
Handle::AerSeq => (self.events.latest_seq().to_string().len(), MODE_FILE | 0o444),
Handle::PciehpSeq => (self.events.latest_seq().to_string().len(), MODE_FILE | 0o444),
Handle::SchemeRoot => return Err(Error::new(EBADF)),
};
stat.st_size = len as u64;
@@ -186,6 +212,39 @@ impl SchemeSync for PciScheme {
addr: _,
ref mut st,
} => Self::read_channel(st, buf),
Handle::Aer => {
let data = self.events.aer_snapshot();
let bytes = data.as_bytes();
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
if offset >= bytes.len() {
return Ok(0);
}
let count = (bytes.len() - offset).min(buf.len());
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
Ok(count)
}
Handle::Pciehp => {
let data = self.events.pciehp_snapshot();
let bytes = data.as_bytes();
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
if offset >= bytes.len() {
return Ok(0);
}
let count = (bytes.len() - offset).min(buf.len());
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
Ok(count)
}
Handle::AerSeq | Handle::PciehpSeq => {
let data = self.events.latest_seq().to_string();
let bytes = data.as_bytes();
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
if offset >= bytes.len() {
return Ok(0);
}
let count = (bytes.len() - offset).min(buf.len());
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
Ok(count)
}
Handle::SchemeRoot => Err(Error::new(EBADF)),
_ => Err(Error::new(EBADF)),
}
@@ -219,7 +278,13 @@ impl SchemeSync for PciScheme {
return Ok(buf);
}
Handle::Device => DEVICE_CONTENTS,
Handle::Access | Handle::Config { .. } | Handle::Channel { .. } => {
Handle::Access
| Handle::Config { .. }
| Handle::Channel { .. }
| Handle::Aer
| Handle::Pciehp
| Handle::AerSeq
| Handle::PciehpSeq => {
return Err(Error::new(ENOTDIR));
}
Handle::SchemeRoot => return Err(Error::new(EBADF)),
@@ -383,11 +448,16 @@ impl PciScheme {
if self.pcie.has_ecam() { 4096 } else { 256 }
}
pub fn new(pcie: Pcie) -> Self {
pub fn set_event_log(&mut self, events: std::sync::Arc<crate::events::EventLog>) {
self.events = events;
}
pub fn new(pcie: std::sync::Arc<Pcie>) -> Self {
Self {
handles: HandleMap::new(),
pcie,
tree: BTreeMap::new(),
events: std::sync::Arc::new(crate::events::EventLog::new()),
}
}
fn parse_after_pci_addr(&mut self, addr: PciAddress, after: &str) -> Result<Handle> {
+23 -10
View File
@@ -130,16 +130,6 @@ fn main() {
scheduler
.schedule_start_and_report_errors(&mut unit_store, UnitId("00_logd.service".to_owned()));
scheduler.step(&mut unit_store, &mut init_config);
// NOTE: init/daemon stdio is deliberately kept on the kernel console
// rather than being redirected into /scheme/log. Redirecting made the boot
// "go dark" after logd on text/recovery ISOs (bare/mini): the graphical
// bootlog (fbbootlogd, VT1) does not reliably surface /scheme/log to the
// framebuffer, and logd's console mirror is racy, so the console showed
// nothing between logd and the login VT — indistinguishable from a hang.
// Keeping stdio on the console makes the full boot observable on
// `-serial` and the framebuffer. (A quiet/splash mode for the graphical
// full ISO can re-enable the redirect once fbbootlogd rendering is fixed.)
let _ = switch_stdio;
let runtime_target = UnitId("00_runtime.target".to_owned());
scheduler.schedule_start_and_report_errors(&mut unit_store, runtime_target.clone());
@@ -155,6 +145,29 @@ fn main() {
Path::new("/usr"),
Path::new("/etc"),
);
// Pin init/daemon stdio to the dedicated boot-log VT (fbcon/1) before the
// /usr units run. Rationale: stdio otherwise stays on the kernel console
// (/scheme/debug), which fbcond renders onto the *active* VT. Once the
// `29_activate_console` unit switches the active VT to 2 for the getty
// login, any daemon that logs afterwards — late async driver attach
// (i2c-hidd, evdevd, dw-acpi-i2cd) or smolnetd's bounded NIC wait — paints
// its output on top of the login banner/prompt, so the prompt is never the
// last thing on screen. Pinning daemon output to VT1 (a fixed VT, not the
// active one) keeps the boot log fully visible — VT1 is the active console
// during boot, and fbcond still mirrors it to the serial line — while
// leaving VT2 clean for a login prompt that lands last.
//
// This is deliberately NOT the old `/scheme/log` redirect that made boot
// "go dark": that depended on fbbootlogd surfacing /scheme/log to the
// framebuffer (racy/unreliable). A real VT renders directly, so full-boot
// observability is preserved. Best-effort: if fbcon/1 cannot be opened
// (e.g. fbcond absent on a headless/serial-only boot), stdio stays on the
// kernel console and the boot remains observable exactly as before.
if let Err(err) = switch_stdio("/scheme/fbcon/1") {
eprintln!("INIT: keeping stdio on kernel console (fbcon/1 unavailable: {err})");
}
{
// FIXME introduce multi-user.target unit and replace the config dir iteration
// scheduler.schedule_start_and_report_errors(&mut unit_store, UnitId("multi-user.target".to_owned()));
+47 -11
View File
@@ -35,22 +35,58 @@ fn get_all_network_adapters() -> Result<Vec<String>> {
adapters.push(scheme);
}
if adapters.is_empty() {
// No NIC present is a normal state on a diskless/headless/bare machine,
// or simply before an unsupported NIC's driver attaches. Report it as
// info and let the caller exit cleanly instead of emitting a spurious
// ERROR + non-zero exit on every boot of a NIC-less system.
info!("no network adapter found; network stack idle");
return Ok(adapters);
}
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
Ok(adapters)
}
/// Wait for at least one NIC driver to register its `network.*` scheme.
///
/// driver-manager attaches drivers ASYNCHRONOUSLY, so on a real boot the NIC's
/// `network.*` scheme does not exist yet when smolnetd starts (10_smolnetd only
/// requires_weak that driver-manager has *started*). The previous code scanned
/// `/scheme` exactly once and, finding nothing, exited — losing the race with
/// virtio-netd and leaving `/scheme/netcfg` absent, so dhcpd/netctl failed with
/// "Can't open /scheme/netcfg/ifaces/eth0/mac". Poll for a bounded window
/// instead. smolnetd is oneshot_async, so this wait never blocks init/boot; a
/// genuinely NIC-less machine simply gets an empty list after the window and
/// exits cleanly.
fn wait_for_network_adapters() -> Vec<String> {
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_secs(20);
let mut logged_wait = false;
loop {
match get_all_network_adapters() {
Ok(adapters) if !adapters.is_empty() => {
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
return adapters;
}
_ => {}
}
if Instant::now() >= deadline {
return Vec::new();
}
if !logged_wait {
info!("no network adapter yet; waiting for a NIC driver to attach...");
logged_wait = true;
}
std::thread::sleep(Duration::from_millis(250));
}
}
fn run(daemon: daemon::Daemon) -> Result<()> {
let adapters = get_all_network_adapters()?;
let adapters = wait_for_network_adapters();
if adapters.is_empty() {
// Nothing to service; exit cleanly (see get_all_network_adapters).
// No `network.*` scheme registered within the wait window. Two cases:
// (a) genuinely NIC-less machine -> expected, harmless;
// (b) a NIC IS present but its driver never registered `network.*`
// -> a driver bug or a STALE/broken NIC driver binary that no
// longer speaks the current scheme protocol.
// If this box has a NIC, this is case (b): rebuild the net driver
// (virtio-netd / e1000d / rtl8168d). Exit cleanly either way (smolnetd
// is oneshot_async, so waiting never blocked boot/login).
warn!(
"no network.* scheme after waiting; network stack idle. If a NIC is \
present, its driver failed to register it (driver bug or stale driver binary)."
);
return Ok(());
}
trace!("found {} network adapter(s)", adapters.len());