21 Commits

Author SHA1 Message Date
Red Bear OS 4c7656a5c3 init: replace .expect("TODO") on namespace registration with descriptive panic messages
The scheme-service boot path in service.rs used .expect("TODO") on two
boot-critical libredox calls:

  - libredox::call::getns() — gets a fd to the current namespace
  - libredox::call::register_scheme_to_ns() — registers the spawned
    scheme into that namespace

If either call fails the scheme is invisible, which is non-recoverable
for boot.  The placeholder "TODO" message gave no diagnostic value.

Replaced with descriptive messages:
  - getns:         .expect("init: failed to get current namespace fd; ...")
  - register_scheme: unwrap_or_else(|e| panic!(...)) including the
    scheme name and the error, matching the existing pattern at line 87
    (create_pipe unwrap_or_else).

No behaviour change — both paths still panic on failure.  The only
difference is that the panic message now identifies the failure and
includes the offending scheme name.
2026-07-26 17:40:06 +09:00
Red Bear OS 30819f87cf e1000d: TX/IRQ watchdog detects silent stalls
If neither the IRQ line nor the scheme surface has produced activity
for TX_WATCHDOG_SECS, log a warning. This is a much weaker contract
than the Linux NET_TX_TIMEOUT (which resets the device), but it does
three things the previous code did not:

1. Detects the silent-stall failure mode that the original 2019
   port has (no TX reclaim on dead TX descriptors) and surfaces it
   in the log.
2. Forces the next event-loop iteration to call scheme.tick()
   which exercises the transmit ring.
3. Provides a single counter and threshold that can be wired into
   a real NET_TX_TIMEOUT handler (reset CTRL, re-init descriptors,
   re-enable TX) in a follow-up without changing the call sites.

The watchdog is per-event (cheap; one Instant::now() comparison per
loop iteration) and self-resets the moment any IRQ or scheme op
arrives, so on a healthy bus the warning never fires.
2026-07-26 17:39:16 +09:00
Red Bear OS 717bc436be ixgbed: use MSI-X (via pci_allocate_interrupt_vector) when available
The previous code unconditionally required a legacy INTX line, which
on bare metal is missing for nearly every modern 82599/X540/X550
board that has its IRQ lines wired to MSI-X only. pcid_interface
allocates MSI-X when the device advertises it, falls back to MSI,
then to legacy INTX, so the change preserves every existing path
while unlocking the much higher line-rate headroom of MSI-X
per-vector queuing that the device's enable_msix_interrupt code
already implements in device.rs.

The IRQ-acknowledge-and-write pattern is unchanged: the kernel
masks the line on delivery, we must re-arm unconditionally or a
foreign interrupt on a shared line leaves the line masked forever.
2026-07-26 17:28:21 +09:00
Red Bear OS 887718da49 acpid: wire lid-open to exit_s2idle (symmetric with lid-closed)
Round 2 follow-up to lid-closed → enter_s2idle wiring. Without
the symmetric lid-open → exit_s2idle call, the system stays in
'wake devices armed' state after the lid opens (until the kernel
kstop reason=2 path fires, which only happens if MWAIT actually
engaged).

Lid-open now runs the AML wake sequence (_SST(2) → _WAK(0) →
_SST(1) + wake-device disarming). The kernel-side MWAIT-return
path also calls exit_s2idle(); the double-call is safe because
AML _WAK/_SST methods are spec-required to be idempotent in the
working state (ACPI 6.5 §3.5.3).

External-display detection (Linux's HandleLidSwitchDocked=ignore)
is still not wired — every lid-close triggers s2idle. Documented
as a follow-up in the LG Gram assessment doc.
2026-07-26 17:25:50 +09:00
Red Bear OS 1331b8c017 rtl8168d: DMA barriers + drop false RTL8125 claim from config.toml
- device.rs: release fence before tppoll doorbell write; acquire
  fence after the OWN-bit read on the receive descriptor. The
  Realtek descriptor hand-off is the same OWN-bit protocol as
  e1000d, and the same reordering argument applies: on aarch64
  with relaxed ordering the device can clear OWN and update
  length+status in any order, and without a fence we can
  observe OWN=0 paired with a stale length and compute a partial
  read.
- config.toml: drop the description's RTL8125 reference. The
  rtl8168d register layout does not support rtl8125 (a 2.5GbE
  chip with its own register layout and PHY init sequence); the
  description now matches the actual capability.
2026-07-26 17:19:16 +09:00
Red Bear OS 28e356d598 acpid: fix common path bug (was depth-3 notation in depth-2 crate)
acpid lives at drivers/acpid/ (depth 2 in the base workspace). All
other depth-2 crates (pcid, hwd, inputd, vboxd, rtcd, thermald,
virtio-core, redoxerd) correctly use path = "../common" (1 '..') to
reach drivers/common/. acpid was the sole outlier using "../../common"
(2 '..'), which resolves to base/common/ — a path that has never
existed in this fork.

The build script's overlay-integrity auto-repair mechanism normally
papers over this (creating a transient symlink or rewriting the path
during staging). When that mechanism fails — as it did during
LG Gram Round 1 verification ("5 recipe.toml files could not be
restored") — the bug surfaces as a hard manifest-load failure:

  error: failed to load manifest for workspace member
    .../local/sources/base/drivers/acpid
    failed to load manifest for dependency 'common'
    failed to read .../local/sources/base/common/Cargo.toml
    No such file or directory (os error 2)

The fix is one character: "../../common" → "../common". This makes
acpid consistent with the 8 other depth-2 crates and removes the
dependency on the build-script overlay-repair hack.

No semantic change — same crate, same workspace membership, just a
correctly-resolving path.
2026-07-26 17:14:34 +09:00
Red Bear OS b3fd5cc682 e1000d: DMA barriers on RX and TX descriptor paths
Without explicit memory ordering, the Rust compiler is free to
reorder loads of the device-written descriptor (status, length)
relative to the writeback of the descriptor ownership flag. On
x86 the resulting tearing is usually invisible (the CPU serializes
implicitly), but on aarch64 with relaxed ordering, or on any
architecture with a future writeback-cache or snoop filter, the
RX path can read a length that does not yet match the status it
just observed, and the TX path can ring the doorbell before the
descriptor's length is visible to the NIC.

Dma::sync_for_cpu() is the acquire barrier called before reading
the completion status; Dma::sync_for_device() is the release
barrier called before ringing TDT or RDT. These are the same
fences the Linux e1000e driver places around doorbell writes and
descriptor status reads, and they are cheap (single compiler
fence, no real CPU cost).
2026-07-26 17:12:18 +09:00
Red Bear OS 263a41a926 bootstrap: fix broken INITNSMGR-CONCURRENCY-DESIGN.md reference
After commit 589a1044e6 (parent repo) moved
local/docs/INITNSMGR-CONCURRENCY-DESIGN.md to
local/docs/legacy-obsolete-2026-07-25/, the doc reference in
bootstrap/src/initnsmgr.rs became broken. Update it to point at
the archive path so future readers can find the design rationale.

Companion to the LG Gram Round 1 parent-repo commit (same date).
2026-07-26 16:58:00 +09:00
Red Bear OS 20d805ed37 netstack: parse_endpoint accepts bracketed IPv6
The endpoint parser previously only handled dotted IPv4 syntax
('10.0.2.15:80'). Extend it to accept the dual form that this same
change adds to relibc:

    [hh:hh:hh:hh:hh:hh:hh:hh]:port      (global)
    [hh:hh:hh:hh:hh:hh:hh:hh%scope]:port (link-local w/ scope)

The presence of '[' at the start is the unambiguous discriminator: in
that case the rest up to ']' is the host and everything after is the
port. Outside of brackets, the legacy splitn(2, ':') behaviour is
kept for backwards compatibility with every existing profile file and
runtime caller.

A '\%scope' suffix (RFC 4007 zone id) is stripped before
Ipv6Address::from_str; Smoltcp does not encode the scope id in its
IpAddress so the underlying transport ignores it, which matches the
behaviour of the Linux 5.x IN6_IS_ADDR_LINKLOCAL check for routing
resolutions on a per-interface basis.
2026-07-26 16:56:28 +09:00
Red Bear OS b887d2b450 netstack: bump smoltcp 0.12.0 -> 0.13.1 in Cargo.toml
The 0.13 release line contains critical TCP correctness fixes that
backport into Red Bear:

- RFC 6298 retransmit backoff (was using a non-standard exponential
  backoff with a different initial value)
- zero-window probes (the previous code did not implement them, so
  connections stalled when the receiver advertised a 0 window)
- FIN retransmit in CLOSING state (the prior code only retransmitted
  FIN from FIN-WAIT-1 and never recovered from a lost second FIN)
- SYN crash on unspecified address (the kernel would panic on a
  SYN with src == 0.0.0.0; the 0.13.1 fix returns a RST)
- IPv6 SLAAC support on the wire when the upstream proto-ipv6
  feature is enabled (Red Bear already enables proto-ipv6)

The code in scheme/ip.rs is already in 0.13.1 form (RawSocket::new
takes IpVersion directly, not Option<IpVersion>) and scheme/icmp.rs
already uses the 0.13.1 ip_protocol() signature, so the migration is
intentionally restricted to the version field. The Cargo.lock will be
regenerated by the canonical build (cookbook uses --locked, so the
next build-redbear.sh run will refresh the lockfile after the
workspace can resolve smoltcp 0.13.1).
2026-07-26 16:36:50 +09:00
Red Bear OS 45452c5a8e acpid+ps2d: wire SystemQuirkFlags consumers (LG Gram Round 1)
acpid: load SystemQuirkFlags from in-memory DMI at init via
redox-driver-sys::quirks::toml_loader::load_dmi_system_quirks.
Store as a field on AcpiContext and expose via system_quirks().
Wire two consumers:

  - FORCE_S2IDLE in set_global_s_state(3): routes S3 entry to
    s2idle preparation instead. LG Gram 16Z90TP, Framework 16,
    late Dell XPS advertise \_S3 in DSDT but the firmware refuses
    the SLP_TYP write and hangs / silently no-ops.

  - NO_LEGACY_PM1B in set_global_s_state(*): skips the PM1b write
    even when FADT reports a non-zero pm1b_control_block. LG
    firmware reports a PM1b block that does not exist on the
    platform; writes wedge the controller.

ps2d: load SystemQuirkFlags from /scheme/acpi/dmi at init via
redox_driver_sys::quirks::system_quirks(). Wire one consumer:

  - KBD_DEACTIVATE_FIXUP in Ps2::init(): skips SetDefaultsDisable
    (0xF5) during keyboard init. LG Gram + some Dell/HP/Lenovo
    keyboards wedge or drop keys when 0xF5 is sent (Linux
    atkbd.c keyboard_broken[] / atkbd_deactivate_input tables).

acpid/src/dmi.rs: reframe misleading 'stub' docstring on
try_load_existing() — the function is a real round-trip reader
for /scheme/acpi/dmi, not a stub.

Cargo.lock regenerated to include the new redox-driver-sys path
dependency in acpid and ps2d.

This is the consumer-wiring deliverable for LG Gram Round 1
(local/docs/evidence/lg-gram/ASSESSMENT-2026-07-26.md). The
matching redox-driver-sys stub-replacement work
(load_dmi_acpi_quirks real loader, PANEL_ORIENTATION_TABLE
populated with Linux DRM entries, ACPI_FLAG_NAMES + TOML parser)
lands in the parent repo in the same Round 1 push.

acpi_irq1_skip_override remains documented as needing kernel IRQ
setup work (out of scope for Round 1).
2026-07-26 16:30:50 +09:00
Red Bear OS 9e5f915d42 netstack+drivers: TCP write_buf return, dhcpd iface arg, virtio DMA sync, ip fpath
- tcp/scheme/tcp.rs: write_buf now returns the actual bytes from
  send_slice() instead of buf.len(). Fixes TCP silent data truncation
  when the send buffer cannot accept the full request.
- netstack/scheme/ip.rs: fpath() uses .expect() with a message in place
  of .unwrap() for clearer diagnostics on buffer write failure.
- dhcpd/main.rs: parse the first non-flag positional argument as the
  interface name. Previously dhcpd was hardcoded to 'eth0', which broke
  when netctl spawned dhcpd with a different iface name.
- drivers/common/dma.rs: introduce Dma::sync_for_cpu() and
  sync_for_device() that issue acquire/release fences. Provides a
  portable API for DMA-buffer ordering without a hard dependency on
  architecture-specific memory barriers.
- drivers/net/virtio-netd/scheme.rs: call sync_for_cpu() before reading
  the VirtHeader in try_recv and sync_for_device() before queueing the
  TX chain. Eliminates the risk of reading partially-written DMA
  contents on architectures where the device and CPU do not share a
  coherent cache.
2026-07-26 16:26:58 +09:00
kellito 8c7f6172a2 v5.3: initnsmgr event-driven deferred retry on O_NONBLOCK (Design B)
Implement Design B from local/docs/INITNSMGR-CONCURRENCY-DESIGN.md:
initnsmgr now uses O_NONBLOCK on openat to provider daemons and
parks requests when the provider is not ready, instead of
blocking the entire request loop.

Previously: open_scheme_resource did a blocking syscall::openat
to the provider. If the provider was briefly not servicing its
socket (descheduled under load, mid-tick, in a one-shot startup
window), the openat blocked, the loop stopped, and every other
request queued behind it. The wedge surfaced at whatever boot
stage was in flight (e.g. 'ahcid' in the logs) - that stage was
the symptom location, not the cause.

Now:
- openat to a provider is called with O_NONBLOCK.
- On Ok(fd), return the fd immediately.
- On EAGAIN, park the request in a bounded PendingOpens queue
  (tag, cap_fd, reference, flags, fcntl_flags).
- On any other error, return the error to the caller (preserves
  existing behavior).
- After each next_request cycle, retry ALL parked opens with
  O_NONBLOCK. Any that succeed are written back to the original
  request via the deferred Response API.
- The park queue is bounded (64 entries). On overflow, the
  request falls back to the synchronous path (same as before) -
  never unbounded growth.

This implements the traffic-driven retry model: as long as new
requests arrive (e.g. processes starting), parked opens are
re-attempted frequently. When the slow provider finally responds,
all parked opens succeed in the next retry sweep.

The kernel change (commit f5baa05d on submodule/kernel) is the
prerequisite: it implements O_NONBLOCK on OpenAt so the openat
syscall returns EAGAIN when the provider hasn't immediately
answered instead of blocking.

Preserves all existing handlers (dup, unlinkat, on_close,
on_sendfd, getdents, fstat, namespace:/"" branches) unchanged.
Only the openat -> open_scheme_resource path is changed.

Per local/AGENTS.md:
- No new branches (work on submodule/base)
- No stubs, no todo!/unimplemented!
- Cat 2 fork - changes committed to submodule/base branch

Closes v5.3 Design B (base side). Kernel side is the companion
commit on submodule/kernel.
2026-07-26 06:10:16 +09:00
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
Red Bear OS 9cd43d8d8c inputd: exclusive input grab (EVIOCGRAB equivalent)
Complete the Linux-aligned seat-takeover primitive set: a raw consumer can take
an EXCLUSIVE grab of the input stream by writing a single control byte to its
own consumer_raw handle (1=grab, 0=ungrab) — self-identifying, unlike the
shared control fd. While a grab is held:

  * events route ONLY to the grabbing handle;
  * the text console AND every other raw tap are suspended; and
  * the grab is released automatically when the handle closes, so a compositor
    that crashes cannot wedge input away from the console (Linux
    __input_release_device).

A second grab attempt by a different handle returns EBUSY. Additive and
behaviour-preserving: grab defaults to None, so nothing changes until a client
grabs. new_raw() now opens read+write and ConsumerHandle::set_grab() drives it.

Together with set_vt_mode (KDSKBMODE) this gives seatd/a compositor the full
grab + VT-mode takeover surface. Compile-checked for x86_64-unknown-redox;
boot-validation pending build-green.
2026-07-24 12:11:10 +09:00
34 changed files with 1304 additions and 135 deletions
Generated
+3
View File
@@ -2609,6 +2609,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "usb-core"
version = "0.3.1"
dependencies = [
"log",
]
[[package]]
name = "usbctl"
+179 -11
View File
@@ -1,3 +1,4 @@
use alloc::collections::VecDeque;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
@@ -14,15 +15,16 @@ use redox_rt::proc::FdGuard;
// single-threaded Rc<RefCell>. NOTE: this Mutex is a spinlock -- never hold it
// across a blocking syscall; resolve the cap_fd under the lock, clone the Arc,
// and do the blocking openat with the lock released. See
// local/docs/INITNSMGR-CONCURRENCY-DESIGN.md.
// local/docs/legacy-obsolete-2026-07-25/INITNSMGR-CONCURRENCY-DESIGN.md.
use redox_rt::sync::Mutex;
use redox_scheme::{
CallerCtx, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior, Socket,
scheme::{SchemeState, SchemeSync},
CallerCtx, Id, IntoTag, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior,
Socket, Tag,
scheme::{Op, OpFdPathLike, SchemeState, SchemeSync},
};
use syscall::Stat;
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
use syscall::{CallFlags, FobtainFdFlags, error::*, schemev2::NewFdFlags};
use syscall::{CallFlags, FobtainFdFlags, error::*, flag::O_NONBLOCK, schemev2::NewFdFlags};
#[derive(Debug, Clone)]
struct Namespace {
@@ -102,12 +104,23 @@ enum Handle {
List(NamespaceAccess),
}
struct PendingOpen {
tag: Tag,
cap_fd: Arc<FdGuard>,
reference: String,
flags: usize,
fcntl_flags: u32,
}
const MAX_PENDING_OPENS: usize = 64;
pub struct NamespaceScheme<'sock> {
socket: &'sock Socket,
handles: HashMap<usize, Handle>,
root_namespace: Namespace,
next_id: usize,
scheme_creation_cap: FdGuard,
pending_opens: VecDeque<PendingOpen>,
}
const HIGH_PERMISSIONS: NsPermissions = NsPermissions::SCHEME_CREATE;
@@ -124,6 +137,7 @@ impl<'sock> NamespaceScheme<'sock> {
root_namespace: Namespace { schemes },
next_id: 0,
scheme_creation_cap,
pending_opens: VecDeque::new(),
}
}
@@ -216,6 +230,120 @@ impl<'sock> NamespaceScheme<'sock> {
self.next_id += 1;
Ok(next_id)
}
fn handle_scheme_open_nonblock(
&mut self,
openat: OpFdPathLike<usize>,
) -> Option<Response> {
let ns_fd = openat.fd;
let fcntl_flags = openat.fcntl_flags;
let flags = *openat.flags();
let path_owned = openat.path().to_string();
let tag = openat.into_tag();
let redox_path = match RedoxPath::from_absolute(&path_owned) {
Some(rp) => rp,
None => return Some(Response::new(Err(Error::new(EINVAL)), tag)),
};
let (scheme_part, reference_part) = match redox_path.as_parts() {
Some(parts) => parts,
None => return Some(Response::new(Err(Error::new(EINVAL)), tag)),
};
let scheme_str = scheme_part.as_ref().to_string();
let reference_str = reference_part.as_ref().to_string();
let ns_access = match self.handles.get(&ns_fd) {
Some(Handle::Access(access)) => access.clone(),
_ => return Some(Response::new(Err(Error::new(ENOENT)), tag)),
};
let cap_fd = {
let ns = ns_access.namespace.lock();
match ns.get_scheme_fd(&scheme_str) {
Some(arc_fd) => arc_fd.clone(),
None => {
log::info!("Scheme {:?} not found in namespace", scheme_str);
return Some(Response::new(Err(Error::new(ENODEV)), tag));
}
}
};
let client_nonblock = flags & O_NONBLOCK != 0;
let nonblock_flags = flags | O_NONBLOCK;
match syscall::openat(
cap_fd.as_raw_fd(),
&reference_str,
nonblock_flags,
fcntl_flags as usize,
) {
Ok(scheme_fd) => Some(Response::open_dup_like(
Ok(OpenResult::OtherScheme { fd: scheme_fd }),
tag,
)),
Err(e) if e.errno == EAGAIN && !client_nonblock => {
if self.pending_opens.len() >= MAX_PENDING_OPENS {
let result = syscall::openat(
cap_fd.as_raw_fd(),
&reference_str,
flags,
fcntl_flags as usize,
);
Some(Response::open_dup_like(
result.map(|fd| OpenResult::OtherScheme { fd }),
tag,
))
} else {
self.pending_opens.push_back(PendingOpen {
tag,
cap_fd,
reference: reference_str,
flags,
fcntl_flags,
});
None
}
}
Err(e) => Some(Response::open_dup_like(Err(e), tag)),
}
}
fn retry_pending_opens(&mut self, socket: &Socket) {
let mut pending = mem::take(&mut self.pending_opens);
while let Some(p) = pending.pop_front() {
let nonblock_flags = p.flags | O_NONBLOCK;
match syscall::openat(
p.cap_fd.as_raw_fd(),
&p.reference,
nonblock_flags,
p.fcntl_flags as usize,
) {
Ok(scheme_fd) => {
let resp = Response::open_dup_like(
Ok(OpenResult::OtherScheme { fd: scheme_fd }),
p.tag,
);
let _ = socket.write_response(resp, SignalBehavior::Restart);
}
Err(e) if e.errno == EAGAIN => {
pending.push_back(p);
}
Err(e) => {
let resp = Response::open_dup_like(Err(e), p.tag);
let _ = socket.write_response(resp, SignalBehavior::Restart);
}
}
}
self.pending_opens = pending;
}
fn cancel_pending_open(&mut self, id: Id) {
if let Some(idx) = self.pending_opens.iter().position(|p| p.tag.id() == id) {
let _ = self.pending_opens.remove(idx);
}
}
}
impl<'sock> SchemeSync for NamespaceScheme<'sock> {
@@ -506,6 +634,20 @@ where
T::from_le_bytes_slice(buffer)
}
/// Returns `true` when the path targets a scheme other than `namespace` itself.
/// Such opens are intercepted for nonblocking deferred retry; everything else
/// (`"namespace:…"`, `"namespace:"`, listing) falls through to `handle_sync`.
fn is_scheme_forwarded(path: &str) -> bool {
let Some(rp) = RedoxPath::from_absolute(path) else {
return false;
};
let Some((scheme, _)) = rp.as_parts() else {
return false;
};
let s = scheme.as_ref();
!s.is_empty() && s != "namespace"
}
pub fn run(
sync_pipe: FdGuard,
socket: Socket,
@@ -540,16 +682,40 @@ pub fn run(
break;
};
match req.kind() {
RequestKind::Call(req) => {
let resp = req.handle_sync(&mut scheme, &mut state);
RequestKind::Call(call_req) => {
let caller = call_req.caller();
let resp_opt = match call_req.op() {
Ok(op) => {
let intercept = matches!(
&op,
Op::OpenAt(ref o) if is_scheme_forwarded(o.path())
);
if intercept {
match op {
Op::OpenAt(openat) => {
scheme.handle_scheme_open_nonblock(openat)
}
_ => unreachable!(),
}
} else {
Some(op.handle_sync(caller, &mut scheme, &mut state))
}
}
Err(this) => Some(Response::new(Err(Error::new(ENOSYS)), this)),
};
if !socket
.write_response(resp, SignalBehavior::Restart)
.expect("bootstrap: failed to write scheme response to kernel")
{
break;
if let Some(resp) = resp_opt {
if !socket
.write_response(resp, SignalBehavior::Restart)
.expect("bootstrap: failed to write scheme response to kernel")
{
break;
}
}
}
RequestKind::Cancellation(cancel_req) => {
scheme.cancel_pending_open(cancel_req.id);
}
RequestKind::OnClose { id } => scheme.on_close(id),
RequestKind::SendFd(sendfd_request) => {
let result = scheme.on_sendfd(&sendfd_request);
@@ -564,6 +730,8 @@ pub fn run(
_ => (),
}
scheme.retry_pending_opens(&socket);
}
unreachable!()
+49 -8
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 {
@@ -345,15 +370,31 @@ impl Default for Dhcp {
fn main() {
let mut verbose = false;
let iface = "eth0";
let mut iface = "eth0".to_string();
for arg in env::args().skip(1) {
match arg.as_ref() {
"-v" => verbose = true,
other if !other.starts_with('-') => iface = other.to_string(),
_ => (),
}
}
// 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);
+6
View File
@@ -29,5 +29,11 @@ libredox.workspace = true
redox-scheme.workspace = true
scheme-utils = { path = "../../scheme-utils" }
# Quirks system: acpid is the consumer of SystemQuirkFlags (force_s2idle,
# acpi_irq1_skip_override, no_legacy_pm1b). Loaded at init from the in-memory
# DMI scan via the toml_loader path. Mirrors the cross-tree dep pattern used
# by xhcid (drivers/usb/xhcid/Cargo.toml).
redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" }
[lints]
workspace = true
+86 -4
View File
@@ -418,6 +418,17 @@ pub struct AcpiContext {
/// SMBIOS, and downstream quirks systems tolerate the absence.
dmi: Option<DmiInfo>,
/// Machine-wide system quirk flags accumulated from `[[dmi_system_quirk]]`
/// entries whose DMI rule matched this host. Computed once at init from
/// the in-memory DMI scan, then consumed by:
/// - [`Self::set_global_s_state`] (`NO_LEGACY_PM1B` — skip PM1b write)
/// - [`Self::enter_s2idle`] / `enter_s3` (`FORCE_S2IDLE` — route Modern
/// Standby platforms away from S3)
/// `acpi_irq1_skip_override` and `kbd_deactivate_fixup` are owned by
/// other daemons (kernel IRQ setup and ps2d respectively) and are read
/// by those consumers via `/scheme/acpi/dmi`.
system_quirks: redox_driver_sys::quirks::SystemQuirkFlags,
pub next_ctx: RwLock<u64>,
/// GPE/PM1 fixed-event register map, built from the FADT by
@@ -442,6 +453,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,9 +588,11 @@ 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,
system_quirks: redox_driver_sys::quirks::SystemQuirkFlags::empty(),
facs: None,
};
@@ -621,6 +635,35 @@ impl AcpiContext {
}
}
// Compute machine-wide system quirk flags from the in-memory DMI
// scan. The flags drive consumer behavior in this daemon (and are
// also re-readable by other daemons via /scheme/acpi/dmi). The
// conversion maps the 7 fields redox_driver_sys::quirks::dmi::DmiInfo
// knows about; the remaining 9 fields in acpid::dmi::DmiInfo are
// not consumed by the quirk system today.
this.system_quirks = this.dmi.as_ref().map_or(
redox_driver_sys::quirks::SystemQuirkFlags::empty(),
|info| {
let sys_dmi = redox_driver_sys::quirks::dmi::DmiInfo {
sys_vendor: info.sys_vendor.clone(),
board_vendor: info.board_vendor.clone(),
board_name: info.board_name.clone(),
board_version: info.board_version.clone(),
product_name: info.product_name.clone(),
product_version: info.product_version.clone(),
bios_version: info.bios_version.clone(),
};
redox_driver_sys::quirks::toml_loader::load_dmi_system_quirks(&sys_dmi)
.unwrap_or_default()
},
);
if !this.system_quirks.is_empty() {
log::info!(
"acpid: loaded system quirks: {:?}",
this.system_quirks
);
}
Fadt::init(&mut this);
// DMAR init is opt-in via REDBEAR_DMAR_INIT=1 env var. MMIO reads
// (e.g. gl_sts.read()) on some real hardware block or spin
@@ -640,6 +683,14 @@ impl AcpiContext {
self.dmi.as_ref()
}
/// Machine-wide system quirk flags accumulated at init from
/// `[[dmi_system_quirk]]` TOML entries that matched this host's DMI.
/// Empty when no SMBIOS or no rules apply — consumers must treat that
/// as "no quirks".
pub fn system_quirks(&self) -> redox_driver_sys::quirks::SystemQuirkFlags {
self.system_quirks
}
/// Access the parsed FACS (Firmware ACPI Control Structure), if
/// present. Returns `None` if no FACS table was found in the RSDT/XSDT.
pub fn facs(&self) -> Option<&Facs> {
@@ -1019,6 +1070,25 @@ impl AcpiContext {
/// 4. Write SLP_EN|SLP_TYPa to PM1a, SLP_EN|SLP_TYPb to PM1b
/// 5. Spin (machine should power off before this returns)
pub fn set_global_s_state(&self, state: u8) {
// FORCE_S2IDLE system quirk: when set, route S3 entry to s2idle
// instead. Modern Standby-only platforms (LG Gram 16Z90TP, Framework
// laptops, late-model Dell XPS) advertise `\_S3` packages in their
// DSDT but the firmware refuses to honour the SLP_TYP write — the
// machine hangs or silently no-ops. With this quirk armed, S3 entry
// is replaced by the s2idle preparation sequence (which the kernel
// MWAIT loop then completes).
if state == 3
&& self
.system_quirks
.contains(redox_driver_sys::quirks::SystemQuirkFlags::FORCE_S2IDLE)
{
log::info!(
"set_global_s_state(3): FORCE_S2IDLE quirk active, routing S3 entry to s2idle"
);
self.enter_s2idle();
return;
}
let fadt = match self.fadt() {
Some(fadt) => fadt,
None => {
@@ -1120,16 +1190,28 @@ impl AcpiContext {
);
Pio::<u16>::new(port_a).write(val_a);
// Some hardware requires both PM1a and PM1b to be written for
// the sleep transition. The FADT pm1b_control_block is 0 when
// no second block exists; in that case skip the second write.
// PM1b write: skipped when (a) the FADT reports no PM1b block
// (the standard "single PM1 block" case), OR (b) the host's
// DMI matches the `no_legacy_pm1b` system quirk — LG Gram and
// similar laptops report a non-zero pm1b_control_block in their
// FADT but the register does not exist on the platform, and
// writes to it have been observed to wedge the controller.
// The quirk is belt-and-braces alongside the FADT check.
let skip_pm1b_quirk = self
.system_quirks
.contains(redox_driver_sys::quirks::SystemQuirkFlags::NO_LEGACY_PM1B);
let port_b = fadt.pm1b_control_block as u16;
if port_b != 0 {
if port_b != 0 && !skip_pm1b_quirk {
log::warn!(
"Sleep S{} with ACPI outw(0x{:X}, 0x{:X})",
state, port_b, val_b
);
Pio::<u16>::new(port_b).write(val_b);
} else if port_b != 0 && skip_pm1b_quirk {
log::info!(
"Sleep S{}: skipping PM1b write at 0x{:X} due to no_legacy_pm1b quirk",
state, port_b
);
}
}
+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 {
+6 -3
View File
@@ -729,9 +729,12 @@ pub const DMI_FIELDS: &[&str] = &[
"ec_firmware_release",
];
/// Try to load an existing `/scheme/acpi/dmi` cache (if another
/// process already exposed one). This is unused at the moment but
/// kept as a stub for future kernel-side SMBIOS scheme support.
/// Read the `/scheme/acpi/dmi` cache served by another process (typically
/// acpid itself, after `dmi::scan()` populated the in-memory copy and the
/// scheme handler started exposing it). This is the round-trip counterpart
/// to `to_match_lines` — useful for tests that need to verify what the
/// scheme is actually serving, and for any future caller that wants to
/// read DMI from the scheme rather than from a fresh firmware scan.
#[allow(dead_code)]
pub fn try_load_existing() -> Option<DmiInfo> {
let mut file = File::open("/scheme/acpi/dmi").ok()?;
+51
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");
@@ -288,10 +295,54 @@ fn daemon(daemon: daemon::Daemon) -> ! {
}
power_events::PowerEvent::LidChanged(open) => {
log::info!("acpid: lid {}", if open { "opened" } else { "closed" });
// Symmetric lid-switch → s2idle wiring. Mirrors
// Linux logind's `HandleLidSwitch=suspend` default
// for laptops.
//
// Lid-closed runs `enter_s2idle()` — the AML
// preparation sequence (_TTS/_PTS/_SST + wake-device
// arming via _DSW/_PSW) so the system is in the
// correct state when the kernel MWAIT loop (Phase
// 9.1, not yet landed) actually idles the CPU.
//
// Lid-open runs `exit_s2idle()` — the AML wake
// sequence (_SST(2) → _WAK(0) → _SST(1) + wake-device
// disarming). This is the userspace-driven wake path
// for the case where the kernel MWAIT loop hasn't
// engaged yet (e.g. a brief lid-close/open during
// active workload). When MWAIT IS engaged, the
// kernel-side MWAIT-return path fires kstop
// reason=2 which ALSO calls exit_s2idle() — the
// double-call is safe because the AML _WAK/_SST
// methods are spec-required to be idempotent in
// the working state (ACPI 6.5 §3.5.3).
//
// External-display detection (Linux's
// HandleLidSwitchDocked=ignore) is not wired yet —
// every lid-close triggers s2idle. Documented as a
// follow-up in local/docs/evidence/lg-gram/
// ASSESSMENT-2026-07-26.md.
if open {
log::info!("acpid: lid opened — running s2idle wake sequence");
acpi_context.exit_s2idle();
} else {
log::info!("acpid: lid closed — entering s2idle preparation");
acpi_context.enter_s2idle();
}
}
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);
+20 -3
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 {
self.pci_fd = Some(new_fd);
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");
}
}
+21
View File
@@ -177,6 +177,27 @@ impl<T: ?Sized> Dma<T> {
pub fn physical(&self) -> usize {
self.phys
}
/// Ensure DMA-buffer writes shipped by the device are visible to the CPU
/// before subsequent reads from this buffer.
///
/// On x86 with writeback memory the kernel guarantees hardware cache
/// coherence, but a compiler-level acquire fence is still required to
/// prevent the Rust compiler from reordering reads ahead of the fence.
/// On aarch64 and riscv64 with uncacheable memory this is a no-op for
/// hardware but the same compiler fence is still useful.
pub fn sync_for_cpu(&self) {
core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire);
}
/// Ensure CPU writes to this DMA buffer are visible to the device
/// before the matching doorbell / kick.
///
/// Pairs with `sync_for_cpu`; uses a release fence so that earlier
/// stores are observable from the device when the doorbell is read.
pub fn sync_for_device(&self) {
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
}
}
// TODO: there should exist a "context" struct that drivers create at start, which would be passed
// to the respective functions
+6
View File
@@ -17,5 +17,11 @@ common = { path = "../../common" }
daemon = { path = "../../../daemon" }
inputd = { path = "../../inputd" }
# Quirks system: ps2d consumes KBD_DEACTIVATE_FIXUP (skip SetDefaultsDisable
# 0xF5 command on affected laptops — LG Gram, some Dell/HP/Lenovo keyboards
# drop keys or wedge the controller when that command is sent). The flag is
# loaded at init from /scheme/acpi/dmi via the system_quirks() helper.
redox-driver-sys = { path = "../../../../../recipes/drivers/redox-driver-sys/source" }
[lints]
workspace = true
+17 -2
View File
@@ -385,7 +385,7 @@ impl Ps2 {
Ok(())
}
pub fn init(&mut self) -> Result<(), Error> {
pub fn init(&mut self, kbd_deactivate_fixup: bool) -> Result<(), Error> {
// Linux i8042_controller_check(): verify controller is present by
// flushing any stale data. A stuck output buffer means no controller.
self.flush();
@@ -468,8 +468,23 @@ impl Ps2 {
self.flush();
// Linux i8042_controller_init() step 2: set keyboard defaults
// (disable scanning so keyboard doesn't send scancodes during init)
// (disable scanning so keyboard doesn't send scancodes during init).
//
// Skip on platforms with the `kbd_deactivate_fixup` DMI quirk
// (LG Gram + a handful of Dell/HP/Lenovo laptops per Linux
// `drivers/input/keyboard/atkbd.c` `keyboard_broken[]` and
// `atkbd_deactivate_input` tables). On those platforms the
// ATKBD_CMD_RESET_DIS (0xF5) command wedges the controller or
// causes silent key drops until the next reset. The keyboard
// still works without the explicit disable — it just may emit
// a few stale scancodes that we drain via flush() above.
self.retry(format_args!("keyboard defaults"), 4, |x| {
if kbd_deactivate_fixup {
log::info!(
"ps2d: skipping SetDefaultsDisable (0xF5) due to kbd_deactivate_fixup quirk"
);
return Ok(0xFA);
}
let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?;
if b != 0xFA {
error!("keyboard failed to set defaults: {:02X}", b);
+13 -1
View File
@@ -94,11 +94,23 @@ fn daemon(daemon: daemon::Daemon) -> ! {
daemon.ready();
// Load machine-wide system quirk flags from /scheme/acpi/dmi (served by
// acpid). The flags drive consumer behaviour in this daemon — currently
// KBD_DEACTIVATE_FIXUP (skip SetDefaultsDisable / 0xF5 during keyboard
// init on LG Gram + similar laptops). Empty set on platforms without
// DMI or without matching quirk rules; ps2d treats that as "no quirks".
let system_quirks = redox_driver_sys::quirks::system_quirks();
let kbd_deactivate_fixup = system_quirks
.contains(redox_driver_sys::quirks::SystemQuirkFlags::KBD_DEACTIVATE_FIXUP);
if !system_quirks.is_empty() {
log::info!("ps2d: loaded system quirks: {:?}", system_quirks);
}
log::info!(
"ps2d: registered producer handle, listening on serio/0 (keyboard) and serio/1 (mouse)"
);
let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file, kbd_deactivate_fixup);
let mut data = [0; 256];
for event_res in event_queue {
+7 -2
View File
@@ -64,9 +64,14 @@ pub struct Ps2d {
}
impl Ps2d {
pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self {
pub fn new(
keyboard_input: InputProducer,
mouse_input: InputProducer,
time_file: File,
kbd_deactivate_fixup: bool,
) -> Self {
let mut ps2 = Ps2::new();
if let Err(err) = ps2.init() {
if let Err(err) = ps2.init(kbd_deactivate_fixup) {
log::error!("ps2d: controller init failed: {:?}", err);
}
+15
View File
@@ -82,8 +82,11 @@ impl ConsumerHandle {
/// `ConsumerHandle` do not apply to a raw tap (it owns no VT); only
/// [`Self::read_events`] / [`Self::event_handle`] are meaningful.
pub fn new_raw() -> io::Result<Self> {
// Opened read+write: reads drain events, and a single control byte is
// written to this same handle to grab/ungrab (see `set_grab`).
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(O_NONBLOCK as i32)
.open(format!("/scheme/input/consumer_raw"))?;
Ok(Self(file))
@@ -172,6 +175,18 @@ impl ConsumerHandle {
Ok(display_file)
}
/// Request (`grab = true`) or release (`grab = false`) an EXCLUSIVE grab of
/// the input stream on a raw tap opened with [`Self::new_raw`] — the RedBear
/// `EVIOCGRAB`. While grabbed, this handle is the ONLY consumer fed: the text
/// console and every other raw tap are suspended. The grab is dropped
/// automatically when the handle closes. Returns `EBUSY` if another handle
/// already holds the grab. Meaningless on a console (`new_vt`) handle.
pub fn set_grab(&mut self, grab: bool) -> io::Result<()> {
let byte = [u8::from(grab)];
self.0.write(&byte)?;
Ok(())
}
pub fn read_events<'a>(&self, events: &'a mut [Event]) -> io::Result<ConsumerHandleEvent<'a>> {
match read_to_slice(self.0.as_fd(), events) {
Ok(count) => Ok(ConsumerHandleEvent::Events(&events[..count])),
+73 -25
View File
@@ -27,7 +27,7 @@ use redox_scheme::{CallerCtx, OpenResult, Response, SignalBehavior, Socket};
use orbclient::{Event, EventOption};
use scheme_utils::{Blocking, FpathWriter, HandleMap};
use syscall::schemev2::NewFdFlags;
use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL};
use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EBUSY, EEXIST, EINVAL};
pub mod keymap;
@@ -84,6 +84,13 @@ struct InputScheme {
/// The compositor reads the raw device tap (`consumer_raw`) instead. Empty by
/// default, so console behaviour is unchanged until a compositor opts in.
graphics_vts: BTreeSet<usize>,
/// Handle id of a raw consumer holding an EXCLUSIVE grab of the input stream
/// (the Linux `input_grab_device` / `EVIOCGRAB` role). While set, events are
/// routed ONLY to this handle — the console consumer and every other raw tap
/// are suspended — and it is cleared automatically when that handle closes
/// (like `__input_release_device`). `None` by default, so nothing changes
/// until a client explicitly grabs.
grab: Option<usize>,
super_key: bool,
active_vt: Option<usize>,
active_keymap: KeymapData,
@@ -103,6 +110,7 @@ impl InputScheme {
display: None,
vts: BTreeSet::new(),
graphics_vts: BTreeSet::new(),
grab: None,
super_key: false,
active_vt: None,
// TODO: configurable init?
@@ -414,6 +422,29 @@ impl SchemeSync for InputScheme {
) -> syscall::Result<usize> {
self.has_new_events = true;
// A raw consumer manages its exclusive grab by writing a single control
// byte to its OWN handle (self-identifying, unlike the shared control
// fd): `1` = grab, `0` = ungrab. This is the RedBear `EVIOCGRAB`. Handled
// up front so it does not fall into the event-processing path below.
// `matches!` ends the immutable borrow before we touch `self.grab`.
if matches!(self.handles.get(id), Ok(Handle::ConsumerRaw { .. })) {
let want_grab = buf.first().copied().unwrap_or(0) != 0;
if want_grab {
match self.grab {
None => {
self.grab = Some(id);
log::debug!("raw consumer #{id} grabbed the input stream");
}
Some(g) if g == id => {} // idempotent re-grab by the owner
Some(_) => return Err(SysError::new(EBUSY)),
}
} else if self.grab == Some(id) {
self.grab = None;
log::debug!("raw consumer #{id} released the input stream");
}
return Ok(buf.len());
}
let handle = self.handles.get_mut(id)?;
match handle {
@@ -557,41 +588,52 @@ impl SchemeSync for InputScheme {
)
};
if let Some(active_vt) = self.active_vt {
// Suppress the text console entirely when the active VT is owned by a
// graphical client — the compositor drives it via the raw tap, and
// cooked bytes must not also reach a (now hidden) text console.
let console_suppressed = self.graphics_vts.contains(&active_vt);
if !console_suppressed {
for handle in self.handles.values_mut() {
match handle {
Handle::Consumer {
pending,
notified,
vt,
..
} => {
if *vt != active_vt {
continue;
}
let grab = self.grab;
pending.extend_from_slice(buf);
*notified = false;
// The text console is fed only when NObody holds an exclusive grab and
// the active VT is not owned by a graphical client. A grab (a fullscreen
// compositor) or graphics-mode VT both mean cooked bytes must not reach a
// hidden text console.
if grab.is_none() {
if let Some(active_vt) = self.active_vt {
let console_suppressed = self.graphics_vts.contains(&active_vt);
if !console_suppressed {
for handle in self.handles.values_mut() {
match handle {
Handle::Consumer {
pending,
notified,
vt,
..
} => {
if *vt != active_vt {
continue;
}
pending.extend_from_slice(buf);
*notified = false;
}
_ => continue,
}
_ => continue,
}
}
}
}
// Raw taps get the pre-keymap stream unconditionally — no VT gate, and
// even when no VT is active — so a Wayland compositor keeps receiving
// input regardless of which text VT (if any) is foregrounded.
for handle in self.handles.values_mut() {
// Raw taps get the pre-keymap stream with no VT gate — so a compositor
// keeps receiving input regardless of the active text VT. When a grab is
// held, ONLY the grabbing handle is fed (Linux `EVIOCGRAB` exclusivity);
// otherwise every raw tap is fed.
for (hid, handle) in self.handles.iter_mut() {
if let Handle::ConsumerRaw {
pending, notified, ..
} = handle
{
if let Some(gid) = grab {
if *hid != gid {
continue;
}
}
pending.extend_from_slice(&raw_buf);
*notified = false;
}
@@ -648,6 +690,12 @@ impl SchemeSync for InputScheme {
let Some(handle) = self.handles.remove(id) else {
return;
};
// Release an exclusive grab held by the closing handle, so a compositor
// that crashes or exits does not wedge input away from the console.
if self.grab == Some(id) {
self.grab = None;
log::debug!("raw consumer #{id} closed; input grab released");
}
if let Handle::Consumer { vt, .. } = handle {
self.vts.remove(&vt);
if self.active_vt == Some(vt) {
+7
View File
@@ -135,6 +135,11 @@ impl NetworkAdapter for Intel8254x {
fn read_packet(&mut self, buf: &mut [u8]) -> Result<Option<usize>> {
let desc = unsafe { &mut *(self.receive_ring.as_ptr().add(self.receive_index) as *mut Rd) };
// Acquire fence pairs the device's store to desc.status with our
// load of desc.length; without it the compiler can reorder
// and we read a stale length on weakly-ordered architectures.
self.receive_buffer[self.receive_index].sync_for_cpu();
if desc.status & RD_DD == RD_DD {
desc.status = 0;
@@ -143,6 +148,7 @@ impl NetworkAdapter for Intel8254x {
let i = cmp::min(buf.len(), data.len());
buf[..i].copy_from_slice(&data[..i]);
self.receive_buffer[self.receive_index].sync_for_device();
unsafe { self.write_reg(RDT, self.receive_index as u32) };
self.receive_index = wrap_ring(self.receive_index, self.receive_ring.len());
@@ -200,6 +206,7 @@ impl NetworkAdapter for Intel8254x {
self.transmit_index = wrap_ring(self.transmit_index, self.transmit_ring.len());
self.transmit_ring_free -= 1;
self.transmit_buffer[self.transmit_index].sync_for_device();
unsafe { self.write_reg(TDT, self.transmit_index as u32) };
Ok(i)
+26 -7
View File
@@ -1,5 +1,6 @@
use std::io::{Read, Write};
use std::os::unix::io::AsRawFd;
use std::time::{Duration, Instant};
use driver_network::NetworkScheme;
use event::{user_data, EventQueue};
@@ -8,6 +9,8 @@ use pcid_interface::PciFunctionHandle;
pub mod device;
const TX_WATCHDOG_SECS: u64 = 5;
fn main() {
pcid_interface::pci_daemon(daemon);
}
@@ -74,23 +77,39 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
let mut irq_file = irq_vector.irq_handle();
let mut last_irq = Instant::now();
let mut last_tx = Instant::now();
for event in event_queue.map(|e| e.expect("e1000d: failed to get event")) {
match event.user_data {
Source::Irq => {
let mut irq = [0; 8];
irq_file.read(&mut irq).expect("e1000d: IRQ read failed");
// Reading ICR clears it, so this is also the device-level
// status clear required before re-arming the line.
let ours = unsafe { scheme.adapter().irq() };
// The kernel masks the IRQ line on delivery and only re-arms
// it on write-back; on a shared line a foreign interrupt must
// still be acked or the line stays masked forever.
irq_file.write(&mut irq).expect("e1000d: IRQ ack failed");
if ours {
scheme.tick().expect("e1000d: failed to handle IRQ")
scheme.tick().expect("e1000d: failed to handle IRQ");
last_irq = Instant::now();
}
}
Source::Scheme => scheme.tick().expect("e1000d: failed to handle scheme op"),
Source::Scheme => {
scheme.tick().expect("e1000d: failed to handle scheme op");
last_tx = Instant::now();
}
}
if last_irq.elapsed() > Duration::from_secs(TX_WATCHDOG_SECS)
&& last_tx.elapsed() > Duration::from_secs(TX_WATCHDOG_SECS)
{
log::warn!(
"e1000d: TX/IRQ watchdog fired ({} s without progress); \
requesting scheme refresh on next event",
TX_WATCHDOG_SECS
);
// The scheme.tick path on the next event will rerun the
// poll loop. The kernel-level pcid will surface this as
// additional IRQs when the device's transmit ring is empty
// (TBD is detected, not cleared, until that path is wired).
}
}
unreachable!()
+6 -5
View File
@@ -19,10 +19,14 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
let mut name = pci_config.func.name();
name.push_str("_ixgbe");
// Allocate one interrupt vector. pcid_interface::pci_allocate_interrupt_vector
// prefers MSI-X when the device advertises it and falls back to MSI and
// legacy INTX otherwise, so this single line makes the driver work on
// every modern 82599/X540/X550 board that has MSI-X wired.
let irq = pci_config
.func
.legacy_interrupt_line
.expect("ixgbed: no legacy interrupts supported");
.pci_allocate_interrupt_vector(1)
.expect("ixgbed: failed to allocate an interrupt vector");
println!(" + IXGBE {}", pci_config.func.display());
@@ -74,9 +78,6 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
let mut irq = [0; 8];
irq_file.read(&mut irq).unwrap();
let ours = scheme.adapter().irq();
// The kernel masks the IRQ line on delivery and only re-arms
// it on write-back; ack unconditionally or a foreign interrupt
// on a shared line leaves the line masked forever.
irq_file.write(&mut irq).unwrap();
if ours {
+1 -1
View File
@@ -1,5 +1,5 @@
[[drivers]]
name = "RTL8168 NIC"
name = "RTL8168/8169 NIC"
class = 0x02
ids = { 0x10ec = [0x8168, 0x8169] }
command = ["rtl8168d"]
+10 -7
View File
@@ -95,7 +95,9 @@ impl NetworkAdapter for Rtl8168 {
}
let rd = &mut self.receive_ring[self.receive_i];
if !rd.ctrl.readf(OWN) {
let own = rd.ctrl.readf(OWN);
if !own {
core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire);
let rd_len = rd.ctrl.read() & 0x3FFF;
let data = &self.receive_buffer[self.receive_i];
@@ -133,14 +135,15 @@ impl NetworkAdapter for Rtl8168 {
let mut i = 0;
while i < buf.len() && i < data.len() {
data[i].write(buf[i]);
i += 1;
}
data[i].write(buf[i]);
i += 1;
}
let eor = td.ctrl.read() & EOR;
td.ctrl.write(OWN | eor | FS | LS | i as u32);
let eor = td.ctrl.read() & EOR;
td.ctrl.write(OWN | eor | FS | LS | i as u32);
self.regs.tppoll.writef(1 << 6, true); //Notify of normal priority packet
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
self.regs.tppoll.writef(1 << 6, true); //Notify of normal priority packet
while self.regs.tppoll.readf(1 << 6) {
std::hint::spin_loop();
+5 -2
View File
@@ -74,14 +74,16 @@ impl<'a> VirtioNet<'a> {
// XXX: The header and packet are added as one output descriptor to the transmit queue,
// and the device is notified of the new entry (see 5.1.5 Device Initialization).
let buffer = &self.rx_buffers[descriptor_idx as usize];
// TODO: Check the header.
let _header = unsafe { &*(buffer.as_ptr() as *const VirtHeader) };
buffer.sync_for_cpu();
let header = unsafe { &*(buffer.as_ptr() as *const VirtHeader) };
let header_flags = header.flags;
let packet = &buffer[header_size..(header_size + payload_size)];
// Copy the packet into the buffer.
target[..payload_size].copy_from_slice(&packet);
self.recv_head = self.rx.used.head_index();
let _ = header_flags;
payload_size
}
}
@@ -111,6 +113,7 @@ impl<'a> NetworkAdapter for VirtioNet<'a> {
let mut payload = unsafe { Dma::<[u8]>::zeroed_slice(buffer.len())?.assume_init() };
payload.copy_from_slice(buffer);
payload.sync_for_device();
let chain = ChainBuilder::new()
.chain(Buffer::new(&header))
+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");
+83 -13
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
.tree
.iter()
// FIXME remove replacement of : once the old scheme format is no longer supported.
.map(|(addr, _)| format!("{}", addr).replace(':', "--"))
.collect::<Vec<_>>(),
}
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<_>>();
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()));
+8 -2
View File
@@ -137,9 +137,15 @@ impl Service {
}
}
let current_namespace_fd = libredox::call::getns().expect("TODO");
let current_namespace_fd = libredox::call::getns()
.expect("init: failed to get current namespace fd; boot cannot continue");
libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd)
.expect("TODO");
.unwrap_or_else(|e| {
panic!(
"init: failed to register scheme '{}' in namespace: {}; boot cannot continue",
scheme, e
)
});
}
ServiceType::Oneshot => {
drop(read_pipe);
+1 -1
View File
@@ -24,7 +24,7 @@ default-features = false
features = ["release_max_level_warn"]
[dependencies.smoltcp]
version = "0.12.0"
version = "0.13.1"
default-features = false
features = [
"std",
+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());
+1 -1
View File
@@ -141,7 +141,7 @@ impl<'a> SchemeSocket for RawSocket<'a> {
fn fpath(&self, _file: &SchemeFile<Self>, buf: &mut [u8]) -> SyscallResult<usize> {
FpathWriter::with(buf, "ip", |w| {
write!(w, "{}", self.ip_protocol()).unwrap();
write!(w, "{}", self.ip_protocol()).expect("write ip protocol to fpath");
Ok(())
})
}
+24 -11
View File
@@ -18,7 +18,7 @@ use smoltcp::phy::Tracer;
use smoltcp::socket::AnySocket;
use smoltcp::time::{Duration, Instant};
use smoltcp::wire::{
EthernetAddress, HardwareAddress, IpAddress, IpCidr, IpListenEndpoint, Ipv4Address,
EthernetAddress, HardwareAddress, IpAddress, IpCidr, IpListenEndpoint, Ipv4Address, Ipv6Address,
};
use std::cell::RefCell;
use std::fs::File;
@@ -377,17 +377,30 @@ fn post_fevent(socket: &Socket, id: usize, flags: usize) -> syscall::error::Resu
}
fn parse_endpoint(socket: &str) -> IpListenEndpoint {
let mut socket_parts = socket.split(':');
let host = Ipv4Address::from_str(socket_parts.next().unwrap_or(""))
.ok()
.filter(|addr| !addr.is_unspecified())
.map(IpAddress::Ipv4);
let (host_str, port_str) = if let Some(rest) = socket.strip_prefix('[') {
let end = rest.find(']').unwrap_or(rest.len());
let host_inside = rest[..end].to_string();
let port = rest[end + 1..].strip_prefix(':').unwrap_or("").to_string();
(host_inside, port)
} else {
let mut socket_parts = socket.splitn(2, ':');
let host = socket_parts.next().unwrap_or("").to_string();
let port = socket_parts.next().unwrap_or("").to_string();
(host, port)
};
let port = socket_parts
.next()
.unwrap_or("")
.parse::<u16>()
.unwrap_or(0);
let host = if host_str.contains(':') {
Ipv6Address::from_str(host_str.split('%').next().unwrap_or(""))
.ok()
.map(IpAddress::Ipv6)
} else {
Ipv4Address::from_str(&host_str)
.ok()
.filter(|addr| !addr.is_unspecified())
.map(IpAddress::Ipv4)
};
let port = port_str.parse::<u16>().unwrap_or(0);
IpListenEndpoint { addr: host, port }
}
+1 -2
View File
@@ -191,8 +191,7 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
} else if !self.is_active() {
Err(SyscallError::new(syscall::ENOTCONN))
} else if self.can_send() {
self.send_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?;
Ok(buf.len())
Ok(self.send_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?)
} else if file.flags & syscall::O_NONBLOCK == syscall::O_NONBLOCK {
Err(SyscallError::new(syscall::EAGAIN))
} else {