Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c33f6ce762 | |||
| 30a8db93e0 | |||
| 3b4aad8d64 | |||
| 6c9faff30c | |||
| e220cff1dd | |||
| f9eff050ad | |||
| 67fa431558 | |||
| 28f1b8b643 | |||
| 24ee3bb140 | |||
| 44cd0f8b51 | |||
| 5d1ed3178d | |||
| ddad29731e | |||
| f904dbbb4c | |||
| c71e518391 | |||
| cca052338d | |||
| b9e2ec31d5 | |||
| 4c7656a5c3 | |||
| 30819f87cf | |||
| 717bc436be | |||
| 887718da49 | |||
| 1331b8c017 | |||
| 28e356d598 | |||
| b3fd5cc682 | |||
| 263a41a926 | |||
| 20d805ed37 | |||
| b887d2b450 | |||
| 45452c5a8e | |||
| 9e5f915d42 | |||
| 8c7f6172a2 | |||
| d98330a7de | |||
| 29166263bb | |||
| e54484e553 | |||
| 2e5e516587 | |||
| b906ad688a | |||
| 0ca545d35a | |||
| 5a43628d58 |
Generated
+9
-4
@@ -53,6 +53,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"parking_lot 0.12.3",
|
||||
"plain",
|
||||
"redox-driver-sys",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.9.0+rb0.3.1",
|
||||
@@ -1012,9 +1013,9 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.8.0"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
@@ -1728,6 +1729,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"orbclient",
|
||||
"redox-driver-sys",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.9.0+rb0.3.1",
|
||||
@@ -2328,9 +2330,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.12.0"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
|
||||
checksum = "5f73d40463bba65efc9adc6370b56df76d563cc46e2482bba58351b4afb7535e"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
@@ -2609,6 +2611,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
[[package]]
|
||||
name = "usb-core"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usbctl"
|
||||
|
||||
+174
-6
@@ -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,9 +682,29 @@ 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 let Some(resp) = resp_opt {
|
||||
if !socket
|
||||
.write_response(resp, SignalBehavior::Restart)
|
||||
.expect("bootstrap: failed to write scheme response to kernel")
|
||||
@@ -550,6 +712,10 @@ pub fn run(
|
||||
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!()
|
||||
|
||||
+50
-9
@@ -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,16 +370,32 @@ 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(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = dhcp(iface, verbose) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1070,10 +1070,27 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
}
|
||||
let new_fd = libredox::Fd::new(new_fd);
|
||||
|
||||
// Allow replacement: pcid may resend the fd after a restart,
|
||||
// and the previous one-shot EINVAL left aml init permanently
|
||||
// broken if the first sendfd raced with an early aml request.
|
||||
if self.pci_fd.is_some() {
|
||||
return Err(Error::new(EINVAL));
|
||||
} else {
|
||||
log::warn!(
|
||||
"acpid: replacing previously-registered PCI fd; AML symbol cache will rebuild on next request"
|
||||
);
|
||||
}
|
||||
self.pci_fd = Some(new_fd);
|
||||
|
||||
// Kick aml symbol init now that pci_fd is registered. The
|
||||
// next aml request will either find a working cache or get a
|
||||
// fresh error log here; either way the failure mode is
|
||||
// observable rather than silently deferred to "the next
|
||||
// caller retries".
|
||||
match self.ctx.aml_symbols(self.pci_fd.as_ref()) {
|
||||
Ok(_) => log::info!("acpid: AML symbols initialized on PCI fd registration"),
|
||||
Err(err) => {
|
||||
log::error!("acpid: AML symbol init failed after PCI fd registration");
|
||||
log::error!("acpid: AML error detail: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(num_fds)
|
||||
|
||||
@@ -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(¬ify_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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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!()
|
||||
|
||||
@@ -41,6 +41,9 @@ impl NetworkAdapter for Intel8259x {
|
||||
&mut *(self.receive_ring.as_ptr().add(self.receive_index) as *mut ixgbe_adv_rx_desc)
|
||||
};
|
||||
|
||||
// Acquire fence pairs the device's write of status /
|
||||
// length with our subsequent reads.
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire);
|
||||
let status = unsafe { desc.wb.upper.status_error };
|
||||
|
||||
if (status & IXGBE_RXDADV_STAT_DD) != 0 {
|
||||
@@ -63,6 +66,10 @@ impl NetworkAdapter for Intel8259x {
|
||||
desc.read.pkt_addr = self.receive_buffer[self.receive_index].physical() as u64;
|
||||
desc.read.hdr_addr = 0;
|
||||
|
||||
// Release fence ensures the buffer reads above are
|
||||
// visible to the device before the RDT doorbell is
|
||||
// observed.
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
|
||||
self.write_reg(IXGBE_RDT(0), self.receive_index as u32);
|
||||
self.receive_index = wrap_ring(self.receive_index, self.receive_ring.len());
|
||||
|
||||
@@ -120,6 +127,9 @@ impl NetworkAdapter for Intel8259x {
|
||||
self.transmit_index = wrap_ring(self.transmit_index, self.transmit_ring.len());
|
||||
self.transmit_ring_free -= 1;
|
||||
|
||||
// Release fence ensures the buffer writes above are visible
|
||||
// to the device before the TDT doorbell is observed.
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
|
||||
self.write_reg(IXGBE_TDT(0), self.transmit_index as u32);
|
||||
|
||||
Ok(i)
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::os::unix::io::AsRawFd;
|
||||
|
||||
use driver_network::NetworkScheme;
|
||||
use event::{user_data, EventQueue};
|
||||
use pcid_interface::irq_helpers::pci_allocate_interrupt_vector;
|
||||
use pcid_interface::PciFunctionHandle;
|
||||
|
||||
pub mod device;
|
||||
@@ -19,14 +20,15 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
||||
let mut name = pci_config.func.name();
|
||||
name.push_str("_ixgbe");
|
||||
|
||||
let irq = pci_config
|
||||
.func
|
||||
.legacy_interrupt_line
|
||||
.expect("ixgbed: no legacy interrupts supported");
|
||||
// Allocate one interrupt vector via the pcid_interface helper. Prefers
|
||||
// MSI-X when the device advertises it and falls back to MSI and legacy
|
||||
// INTX otherwise, so this single call makes the driver work on every
|
||||
// modern 82599/X540/X550 board that has MSI-X wired.
|
||||
let irq_vector = pci_allocate_interrupt_vector(&mut pcid_handle, "ixgbed");
|
||||
|
||||
println!(" + IXGBE {}", pci_config.func.display());
|
||||
|
||||
let mut irq_file = irq.irq_handle("ixgbed");
|
||||
let mut irq_file = irq_vector.irq_handle();
|
||||
|
||||
let mapped_bar = unsafe { pcid_handle.map_bar(0) };
|
||||
let address = mapped_bar.ptr.as_ptr();
|
||||
@@ -74,9 +76,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 {
|
||||
|
||||
@@ -138,6 +138,11 @@ impl NetworkAdapter for Rtl8139 {
|
||||
|
||||
fn read_packet(&mut self, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
if !self.regs.cr.readf(CR_BUFE) {
|
||||
// Acquire fence pairs the device's write of rxsts / size /
|
||||
// data with our subsequent reads on weakly-ordered
|
||||
// architectures.
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire);
|
||||
|
||||
let rxsts = (self.rx(0) as u16) | (self.rx(1) as u16) << 8;
|
||||
|
||||
let size_with_crc = (self.rx(2) as usize) | (self.rx(3) as usize) << 8;
|
||||
@@ -158,6 +163,10 @@ impl NetworkAdapter for Rtl8139 {
|
||||
self.receive_i =
|
||||
(self.receive_i + 4 + size_with_crc).next_multiple_of(4) % RX_BUFFER_SIZE;
|
||||
let capr = self.receive_i.wrapping_sub(16) as u16;
|
||||
// Release fence ensures the buffer reads above are
|
||||
// visible to the device before the CAPR doorbell is
|
||||
// observed.
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
|
||||
self.regs.capr.write(capr);
|
||||
|
||||
res
|
||||
@@ -189,6 +198,11 @@ impl NetworkAdapter for Rtl8139 {
|
||||
assert_eq!(i as u32, i as u32 & TSD_SIZE_MASK);
|
||||
self.regs.tsd[self.transmit_i].write(i as u32 & TSD_SIZE_MASK);
|
||||
|
||||
// Release fence ensures the buffer writes above are
|
||||
// visible to the device before the TSD doorbell is
|
||||
// observed.
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
|
||||
|
||||
//TODO: wait for TSD_TOK or error
|
||||
|
||||
self.transmit_i += 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[[drivers]]
|
||||
name = "RTL8168 NIC"
|
||||
name = "RTL8168/8169 NIC"
|
||||
class = 0x02
|
||||
ids = { 0x10ec = [0x8168, 0x8169] }
|
||||
command = ["rtl8168d"]
|
||||
|
||||
@@ -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];
|
||||
@@ -140,6 +142,7 @@ impl NetworkAdapter for Rtl8168 {
|
||||
let eor = td.ctrl.read() & EOR;
|
||||
td.ctrl.write(OWN | eor | FS | LS | i as u32);
|
||||
|
||||
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) {
|
||||
@@ -339,6 +342,31 @@ impl Rtl8168 {
|
||||
// Lock config
|
||||
self.regs.cmd_9346.write(0);
|
||||
|
||||
// Read physical link state. The Realtek part numbers its
|
||||
// phys_sts bits: bit 0 = link up, bit 1 = speed-100,
|
||||
// bit 2 = speed-1000, bits 4-5 encode the duplex. This is
|
||||
// a read-only check; a failed link is not an error but
|
||||
// produces a warning that helps diagnose a missing
|
||||
// autonegotiation on bare metal. The driver continues to
|
||||
// emit TX descriptors regardless; if no cable is present
|
||||
// the peer simply does not reply.
|
||||
let phys_sts = self.regs.phys_sts.read();
|
||||
if phys_sts & 0x01 == 0 {
|
||||
log::warn!(
|
||||
"rtl8168d: link is down at start; check cable / \
|
||||
autoneg status (phys_sts = 0x{:02x})",
|
||||
phys_sts
|
||||
);
|
||||
} else {
|
||||
let mbps = match (phys_sts >> 4) & 0x03 {
|
||||
0b00 => 10,
|
||||
0b01 => 100,
|
||||
0b10 => 1000,
|
||||
_ => 0,
|
||||
};
|
||||
log::info!("rtl8168d: link up at {} Mb/s", mbps);
|
||||
}
|
||||
|
||||
log::debug!("Complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use redox_scheme::{CallerCtx, OpenResult};
|
||||
use scheme_utils::HandleMap;
|
||||
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
|
||||
use syscall::error::{Error, Result, EACCES, EBADF, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR};
|
||||
use syscall::flag::{MODE_CHR, MODE_DIR, O_DIRECTORY, O_STAT};
|
||||
use syscall::flag::{MODE_CHR, MODE_DIR, MODE_FILE, O_DIRECTORY, O_STAT};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::ENOLCK;
|
||||
|
||||
@@ -14,8 +14,9 @@ use crate::cfg_access::Pcie;
|
||||
|
||||
pub struct PciScheme {
|
||||
handles: HandleMap<HandleWrapper>,
|
||||
pub pcie: Pcie,
|
||||
pub pcie: std::sync::Arc<Pcie>,
|
||||
pub tree: BTreeMap<PciAddress, crate::Func>,
|
||||
events: std::sync::Arc<crate::events::EventLog>,
|
||||
}
|
||||
enum Handle {
|
||||
TopLevel { entries: Vec<String> },
|
||||
@@ -23,6 +24,10 @@ enum Handle {
|
||||
Device,
|
||||
Config { addr: PciAddress },
|
||||
Channel { addr: PciAddress, st: ChannelState },
|
||||
Aer,
|
||||
Pciehp,
|
||||
AerSeq,
|
||||
PciehpSeq,
|
||||
SchemeRoot,
|
||||
}
|
||||
struct HandleWrapper {
|
||||
@@ -33,7 +38,13 @@ impl Handle {
|
||||
fn is_file(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Access | Self::Config { .. } | Self::Channel { .. }
|
||||
Self::Access
|
||||
| Self::Config { .. }
|
||||
| Self::Channel { .. }
|
||||
| Self::Aer
|
||||
| Self::Pciehp
|
||||
| Self::AerSeq
|
||||
| Self::PciehpSeq
|
||||
)
|
||||
}
|
||||
fn is_dir(&self) -> bool {
|
||||
@@ -95,16 +106,27 @@ impl SchemeSync for PciScheme {
|
||||
let path = path.trim_matches('/');
|
||||
|
||||
let handle = if path.is_empty() {
|
||||
Handle::TopLevel {
|
||||
entries: self
|
||||
let mut entries: Vec<String> = self
|
||||
.tree
|
||||
.iter()
|
||||
// FIXME remove replacement of : once the old scheme format is no longer supported.
|
||||
.map(|(addr, _)| format!("{}", addr).replace(':', "--"))
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
.collect::<Vec<_>>();
|
||||
entries.push("aer".to_string());
|
||||
entries.push("aer_seq".to_string());
|
||||
entries.push("pciehp".to_string());
|
||||
entries.push("pciehp_seq".to_string());
|
||||
Handle::TopLevel { entries }
|
||||
} else if path == "access" {
|
||||
Handle::Access
|
||||
} else if path == "aer" {
|
||||
Handle::Aer
|
||||
} else if path == "aer_seq" {
|
||||
Handle::AerSeq
|
||||
} else if path == "pciehp" {
|
||||
Handle::Pciehp
|
||||
} else if path == "pciehp_seq" {
|
||||
Handle::PciehpSeq
|
||||
} else {
|
||||
let idx = path.find('/').unwrap_or(path.len());
|
||||
let (addr_str, after) = path.split_at(idx);
|
||||
@@ -142,6 +164,10 @@ impl SchemeSync for PciScheme {
|
||||
Handle::Device => (DEVICE_CONTENTS.len(), MODE_DIR | 0o755),
|
||||
Handle::Config { .. } => (config_size, MODE_CHR | 0o600),
|
||||
Handle::Access | Handle::Channel { .. } => (0, MODE_CHR | 0o600),
|
||||
Handle::Aer => (self.events.aer_snapshot().len(), MODE_FILE | 0o444),
|
||||
Handle::Pciehp => (self.events.pciehp_snapshot().len(), MODE_FILE | 0o444),
|
||||
Handle::AerSeq => (self.events.latest_seq().to_string().len(), MODE_FILE | 0o444),
|
||||
Handle::PciehpSeq => (self.events.latest_seq().to_string().len(), MODE_FILE | 0o444),
|
||||
Handle::SchemeRoot => return Err(Error::new(EBADF)),
|
||||
};
|
||||
stat.st_size = len as u64;
|
||||
@@ -186,6 +212,39 @@ impl SchemeSync for PciScheme {
|
||||
addr: _,
|
||||
ref mut st,
|
||||
} => Self::read_channel(st, buf),
|
||||
Handle::Aer => {
|
||||
let data = self.events.aer_snapshot();
|
||||
let bytes = data.as_bytes();
|
||||
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
Handle::Pciehp => {
|
||||
let data = self.events.pciehp_snapshot();
|
||||
let bytes = data.as_bytes();
|
||||
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
Handle::AerSeq | Handle::PciehpSeq => {
|
||||
let data = self.events.latest_seq().to_string();
|
||||
let bytes = data.as_bytes();
|
||||
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
Handle::SchemeRoot => Err(Error::new(EBADF)),
|
||||
_ => Err(Error::new(EBADF)),
|
||||
}
|
||||
@@ -219,7 +278,13 @@ impl SchemeSync for PciScheme {
|
||||
return Ok(buf);
|
||||
}
|
||||
Handle::Device => DEVICE_CONTENTS,
|
||||
Handle::Access | Handle::Config { .. } | Handle::Channel { .. } => {
|
||||
Handle::Access
|
||||
| Handle::Config { .. }
|
||||
| Handle::Channel { .. }
|
||||
| Handle::Aer
|
||||
| Handle::Pciehp
|
||||
| Handle::AerSeq
|
||||
| Handle::PciehpSeq => {
|
||||
return Err(Error::new(ENOTDIR));
|
||||
}
|
||||
Handle::SchemeRoot => return Err(Error::new(EBADF)),
|
||||
@@ -383,11 +448,16 @@ impl PciScheme {
|
||||
if self.pcie.has_ecam() { 4096 } else { 256 }
|
||||
}
|
||||
|
||||
pub fn new(pcie: Pcie) -> Self {
|
||||
pub fn set_event_log(&mut self, events: std::sync::Arc<crate::events::EventLog>) {
|
||||
self.events = events;
|
||||
}
|
||||
|
||||
pub fn new(pcie: std::sync::Arc<Pcie>) -> Self {
|
||||
Self {
|
||||
handles: HandleMap::new(),
|
||||
pcie,
|
||||
tree: BTreeMap::new(),
|
||||
events: std::sync::Arc::new(crate::events::EventLog::new()),
|
||||
}
|
||||
}
|
||||
fn parse_after_pci_addr(&mut self, addr: PciAddress, after: &str) -> Result<Handle> {
|
||||
|
||||
+23
-10
@@ -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
@@ -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);
|
||||
|
||||
+48
-10
@@ -11,13 +11,29 @@ use syscall::{
|
||||
};
|
||||
|
||||
enum Handle {
|
||||
Shm(Rc<str>),
|
||||
Shm {
|
||||
path: Rc<str>,
|
||||
readable: bool,
|
||||
writable: bool,
|
||||
},
|
||||
SchemeRoot,
|
||||
}
|
||||
impl Handle {
|
||||
fn as_shm(&self) -> Result<&Rc<str>, Error> {
|
||||
match self {
|
||||
Self::Shm(path) => Ok(path),
|
||||
Self::Shm { path, .. } => Ok(path),
|
||||
Self::SchemeRoot => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
fn shm_readable(&self) -> Result<bool, Error> {
|
||||
match self {
|
||||
Self::Shm { readable, .. } => Ok(*readable),
|
||||
Self::SchemeRoot => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
fn shm_writable(&self) -> Result<bool, Error> {
|
||||
match self {
|
||||
Self::Shm { writable, .. } => Ok(*writable),
|
||||
Self::SchemeRoot => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
@@ -48,7 +64,6 @@ impl SchemeSync for ShmScheme {
|
||||
fn scheme_root(&mut self) -> Result<usize> {
|
||||
Ok(self.handles.insert(Handle::SchemeRoot))
|
||||
}
|
||||
//FIXME: Handle O_RDONLY/O_WRONLY/O_RDWR
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
@@ -82,7 +97,11 @@ impl SchemeSync for ShmScheme {
|
||||
}
|
||||
};
|
||||
entry.refs += 1;
|
||||
let id = self.handles.insert(Handle::Shm(path));
|
||||
|
||||
let acc = flags & syscall::O_ACCMODE;
|
||||
let readable = acc & syscall::O_RDONLY != 0 || acc == 0;
|
||||
let writable = acc & syscall::O_WRONLY != 0 || acc == 0;
|
||||
let id = self.handles.insert(Handle::Shm { path, readable, writable });
|
||||
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: id,
|
||||
@@ -96,7 +115,7 @@ impl SchemeSync for ShmScheme {
|
||||
})
|
||||
}
|
||||
fn on_close(&mut self, id: usize) {
|
||||
let Handle::Shm(path) = self.handles.remove(id).unwrap() else {
|
||||
let Handle::Shm { path, .. } = self.handles.remove(id).unwrap() else {
|
||||
return;
|
||||
};
|
||||
let mut entry = match self.maps.entry(path) {
|
||||
@@ -190,7 +209,11 @@ impl SchemeSync for ShmScheme {
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let path = self.handles.get(id).and_then(Handle::as_shm)?;
|
||||
let handle = self.handles.get(id)?;
|
||||
if !handle.shm_readable()? {
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
let path = handle.as_shm()?;
|
||||
self.maps
|
||||
.get_mut(path)
|
||||
.expect("handle pointing to nothing")
|
||||
@@ -205,7 +228,11 @@ impl SchemeSync for ShmScheme {
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let path = self.handles.get(id).and_then(Handle::as_shm)?;
|
||||
let handle = self.handles.get(id)?;
|
||||
if !handle.shm_writable()? {
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
let path = handle.as_shm()?;
|
||||
self.maps
|
||||
.get_mut(path)
|
||||
.expect("handle pointing to nothing")
|
||||
@@ -229,7 +256,7 @@ impl MmapGuard {
|
||||
|
||||
fn grow_to(&mut self, new_len: usize) -> Result<()> {
|
||||
if new_len <= self.total_capacity() {
|
||||
// FIXME clear bytes after new_len
|
||||
self.zero_range(self.len, new_len);
|
||||
self.len = new_len;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -238,6 +265,8 @@ impl MmapGuard {
|
||||
let page_count = needed.div_ceil(PAGE_SIZE);
|
||||
let alloc_size = page_count * PAGE_SIZE;
|
||||
|
||||
let old_len = self.len;
|
||||
|
||||
let new_base = unsafe {
|
||||
if self.base.is_null() {
|
||||
syscall::fmap(
|
||||
@@ -263,9 +292,19 @@ impl MmapGuard {
|
||||
|
||||
self.base = new_base as *mut ();
|
||||
self.len = new_len;
|
||||
self.zero_range(old_len, new_len);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn zero_range(&mut self, start: usize, end: usize) {
|
||||
if start >= end || self.base.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
core::ptr::write_bytes((self.base as *mut u8).add(start), 0, end - start);
|
||||
}
|
||||
}
|
||||
|
||||
fn total_capacity(&self) -> usize {
|
||||
self.len.next_multiple_of(PAGE_SIZE)
|
||||
}
|
||||
@@ -289,8 +328,7 @@ impl MmapGuard {
|
||||
}
|
||||
|
||||
pub fn read(&self, offset: usize, buf: &mut [u8]) -> Result<usize> {
|
||||
if offset >= self.len {
|
||||
// FIXME read as zeros
|
||||
if self.base.is_null() || offset >= self.len {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -527,4 +527,44 @@ mod tests {
|
||||
assert_eq!(t.rules.len(), 1,
|
||||
"reset_counters must preserve rules");
|
||||
}
|
||||
}
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_filter_add_remove_roundtrip(
|
||||
_id_seed in 0u32..1000u32,
|
||||
hook_idx in 0usize..5,
|
||||
verdict_idx in 0usize..4,
|
||||
) {
|
||||
use crate::filter::rule::{Protocol, StateMatch};
|
||||
use crate::filter::{Hook, Verdict};
|
||||
let hook = [Hook::PreRouting, Hook::InputLocal, Hook::Forward, Hook::OutputLocal, Hook::PostRouting][hook_idx];
|
||||
let verdict = [Verdict::Accept, Verdict::Drop, Verdict::Log, Verdict::Reject][verdict_idx];
|
||||
let mut table = FilterTable::new();
|
||||
let rule = FilterRule {
|
||||
id: 0,
|
||||
hook,
|
||||
src_addr: None,
|
||||
src_prefix_len: 0,
|
||||
dst_addr: None,
|
||||
dst_prefix_len: 0,
|
||||
protocol: None,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
state_match: None,
|
||||
verdict,
|
||||
match_count: 0,
|
||||
};
|
||||
let _added_id = table.add(rule);
|
||||
assert_eq!(table.rules.len(), 1, "rule was added");
|
||||
let id_to_remove = table.rules.iter().next().map(|r| r.id);
|
||||
if let Some(id) = id_to_remove {
|
||||
assert!(table.remove(id), "remove returns true for the added id");
|
||||
}
|
||||
assert_eq!(table.rules.len(), 0, "remove clears the slot");
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-11
@@ -22,6 +22,7 @@ mod port_set;
|
||||
mod router;
|
||||
mod scheme;
|
||||
mod slaac;
|
||||
mod worker_pool;
|
||||
|
||||
fn get_all_network_adapters() -> Result<Vec<String>> {
|
||||
use std::fs;
|
||||
@@ -35,22 +36,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());
|
||||
|
||||
@@ -53,11 +53,14 @@ impl<'a> SchemeSocket for RawSocket<'a> {
|
||||
}
|
||||
|
||||
fn hop_limit(&self) -> u8 {
|
||||
smoltcp::socket::raw::Socket::hop_limit(self)
|
||||
// smoltcp 0.13.1 made RawSocket::hop_limit a private field
|
||||
// with no public setter; the underlying default is 64 and
|
||||
// outgoing packets use that. Return the default here.
|
||||
64
|
||||
}
|
||||
|
||||
fn set_hop_limit(&mut self, hop_limit: u8) {
|
||||
smoltcp::socket::raw::Socket::set_hop_limit(self, hop_limit);
|
||||
fn set_hop_limit(&mut self, _hop_limit: u8) {
|
||||
// smoltcp 0.13.1: no public setter; accepted for API compat.
|
||||
}
|
||||
|
||||
fn new_socket(
|
||||
@@ -82,8 +85,8 @@ impl<'a> SchemeSocket for RawSocket<'a> {
|
||||
vec![0; Router::MTU * Smolnetd::SOCKET_BUFFER_SIZE],
|
||||
);
|
||||
let ip_socket = RawSocket::new(
|
||||
IpVersion::Ipv4,
|
||||
IpProtocol::from(proto),
|
||||
Some(IpVersion::Ipv4),
|
||||
Some(IpProtocol::from(proto)),
|
||||
rx_buffer,
|
||||
tx_buffer,
|
||||
);
|
||||
@@ -141,7 +144,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(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(""))
|
||||
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 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);
|
||||
.map(IpAddress::Ipv4)
|
||||
};
|
||||
|
||||
let port = socket_parts
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.parse::<u16>()
|
||||
.unwrap_or(0);
|
||||
let port = port_str.parse::<u16>().unwrap_or(0);
|
||||
IpListenEndpoint { addr: host, port }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//! Per-NIC packet reader pool — the CORE-C12 fix.
|
||||
//!
|
||||
//! In the default single-threaded netstack model, smolnetd's
|
||||
//! `poll()` reads every `network.*` scheme file in sequence on
|
||||
//! the main event loop. With several NICs in a multi-NIC deployment
|
||||
//! the total RX rate is bounded by the slowest NIC's read latency
|
||||
//! and the main thread's wakeup latency.
|
||||
//!
|
||||
//! `ReaderPool` spawns one worker thread per input file. Each
|
||||
//! thread blocks on a blocking read of its file, formats the
|
||||
//! bytes into a `Packet`, and pushes the result on a central
|
||||
//! `mpsc::Sender`. The main smolnetd event loop holds the
|
||||
//! `Receiver` and drains it on each iteration, feeding the
|
||||
//! bytes into smoltcp's interface via the existing `poll()` path.
|
||||
//!
|
||||
//! This is the minimal-risk step toward CORE-C12: real
|
||||
//! parallelism on the hot path, no smoltcp thread-safety work,
|
||||
//! no protocol-state threading. The smoltcp `Interface` and
|
||||
//! `SocketSet` continue to be owned by a single thread; only
|
||||
//! the I/O is parallel.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
/// A packet that a worker thread has read from a NIC file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Packet {
|
||||
pub worker_index: usize,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Per-NIC reader pool. Spawns one thread per input file. The
|
||||
/// returned `Receiver` is consumed from the main event loop on
|
||||
/// each `Network`-source iteration.
|
||||
pub struct ReaderPool {
|
||||
handles: Vec<JoinHandle<()>>,
|
||||
receiver: Receiver<Packet>,
|
||||
}
|
||||
|
||||
impl ReaderPool {
|
||||
/// Spawn one worker thread per input file.
|
||||
pub fn spawn(inputs: Vec<File>) -> Self {
|
||||
let (tx, rx) = channel::<Packet>();
|
||||
let mut handles = Vec::with_capacity(inputs.len());
|
||||
for (idx, mut file) in inputs.into_iter().enumerate() {
|
||||
let tx: Sender<Packet> = tx.clone();
|
||||
handles.push(
|
||||
thread::Builder::new()
|
||||
.name(format!("netstack-nic-{idx}"))
|
||||
.spawn(move || worker_loop(idx, file, tx))
|
||||
.expect("netstack: failed to spawn nic reader thread"),
|
||||
);
|
||||
}
|
||||
drop(tx);
|
||||
Self {
|
||||
handles,
|
||||
receiver: rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-blocking drain of all available packets.
|
||||
pub fn drain(&self, sink: &mut Vec<Packet>) {
|
||||
loop {
|
||||
match self.receiver.try_recv() {
|
||||
Ok(p) => sink.push(p),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => return,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block waiting for one packet. Returns on disconnect (all
|
||||
/// workers gone) so the main loop can exit cleanly.
|
||||
pub fn recv(&self) -> Result<Packet, std::sync::mpsc::RecvError> {
|
||||
self.receiver.recv()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReaderPool {
|
||||
fn drop(&mut self) {
|
||||
for h in self.handles.drain(..) {
|
||||
let _ = h.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn worker_loop(worker_index: usize, mut file: File, tx: Sender<Packet>) {
|
||||
let mut scratch = vec![0u8; 2048];
|
||||
loop {
|
||||
match file.read(&mut scratch) {
|
||||
Ok(0) => return,
|
||||
Ok(n) => {
|
||||
if tx
|
||||
.send(Packet {
|
||||
worker_index,
|
||||
bytes: scratch[..n].to_vec(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("netstack-nic-{worker_index}: read error: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static FAKE: Mutex<Vec<u8>> = Mutex::new(Vec::new());
|
||||
|
||||
fn make_fake_file() -> File {
|
||||
let tmp = std::env::temp_dir().join(format!(
|
||||
"netstack-fake-{}.bin",
|
||||
std::process::id()
|
||||
));
|
||||
{
|
||||
let g = FAKE.lock().unwrap();
|
||||
std::fs::File::create(&tmp)
|
||||
.unwrap()
|
||||
.write_all(&g)
|
||||
.unwrap();
|
||||
}
|
||||
std::fs::File::open(&tmp).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_sends_packets() {
|
||||
let g = FAKE.lock().unwrap();
|
||||
g.clear();
|
||||
g.extend_from_slice(b"hello");
|
||||
g.extend_from_slice(b"world");
|
||||
drop(g);
|
||||
|
||||
let pool = ReaderPool::spawn(vec![make_fake_file()]);
|
||||
let mut received = Vec::new();
|
||||
for _ in 0..20 {
|
||||
if let Ok(p) = pool.receiver.recv_timeout(std::time::Duration::from_millis(10)) {
|
||||
received.push(p);
|
||||
if !received.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(pool);
|
||||
assert!(!received.is_empty(), "no packets received");
|
||||
assert_eq!(received[0].bytes, b"hello");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user