From 333feeac8bcf0509e03c2cdac26881b602712b19 Mon Sep 17 00:00:00 2001 From: NB-Group Date: Mon, 3 Aug 2026 01:30:51 +0800 Subject: [PATCH] asusd: open scsi_generic /dev/sgN for SCSI Aura devices SG_IO with a vendor CDB on the block node (/dev/sdX) requires CAP_SYS_RAWIO. The hardened asusd systemd unit strips all capabilities (empty CapabilityBoundingSet, NoNewPrivileges=true), so every SG_IO on the block node returned EPERM. write_effect() was doing perform(task).ok(), so the error was silently swallowed; asusd reported success while zero SCSI traffic reached the enclosure (confirmed via usbmon). Resolve the scsi_generic node (/dev/sgN) backing the block device and open that instead. The sg driver gates access at open() through file permissions, so SG_IO works without any capability, the same path sg3_utils and OpenRGB use. On hotplug the sg node can appear just after the block node, so init retries briefly before falling back to the block device. Also stop discarding the ioctl error in write_effect so a future failure can't go invisible again. Verified on a ROG STRIX Arion (0b05:1932): 16/16 SG_IO return 0, the ENE vendor CDBs (mode 0x8021, colour registers 0x8160+, apply 0x80a0) appear in usbmon, and the LEDs change. --- asusd/src/aura_manager.rs | 61 ++++++++++++++++++++++++++++++++++++-- asusd/src/aura_scsi/mod.rs | 7 ++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index d7c83d939..eebdbeecc 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -276,6 +276,31 @@ impl DeviceManager { Ok(devices) } + /// Resolve the `/dev/sgN` (scsi_generic) node backing a block device. + /// + /// Walks up from the block device to its owning scsi_device and reads the + /// `scsi_generic/sgN` child. Works for whole-disk (`/dev/sda`) and + /// partition (`/dev/sda1`) nodes alike, since the scsi_device is a common + /// ancestor. Returns None if no sg node exists (e.g. the `sg` module is + /// not loaded). + fn sg_node_for_block(device: &Device) -> Option { + let mut current = device.parent(); + while let Some(d) = current { + if let Ok(entries) = std::fs::read_dir(d.syspath().join("scsi_generic")) { + for entry in entries.flatten() { + if let Some(name) = entry.file_name().to_str() { + let node = format!("/dev/{name}"); + if std::path::Path::new(&node).exists() { + return Some(node); + } + } + } + } + current = d.parent(); + } + None + } + async fn init_scsi( connection: &Connection, device: &Device, @@ -289,8 +314,40 @@ impl DeviceManager { .property_value("ID_MODEL_ID") .unwrap_or_default() .to_string_lossy(); - if let Some(dev_str) = dev_node.as_os_str().to_str() { - if let Ok(dev_type) = DeviceHandle::maybe_scsi(dev_str, &prod_id).await { + // SG_IO with vendor commands on the block node (/dev/sdX) + // requires CAP_SYS_RAWIO, which the hardened asusd unit drops + // (every ioctl EPERMs and is silently swallowed by write_effect). + // The scsi_generic /dev/sgN node gates access at open() via + // file permissions instead, so it works with no capabilities, + // the same path sg3_utils / OpenRGB use. + // + // On hotplug the sg node can appear just after the block node, + // so retry briefly before falling back to the block device + // (which would EPERM). At startup the node already exists, so + // the first attempt succeeds with no delay. + let mut sg_node = None; + for attempt in 0..8u8 { + if let Some(sg) = Self::sg_node_for_block(device) { + sg_node = Some(sg); + break; + } + if attempt < 7 { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + } + let dev_str = match sg_node { + Some(sg) => Some(sg), + None => { + warn!( + "No /dev/sgN for SCSI device after retries; falling back to block \ + node {:?} (SG_IO will EPERM unless asusd has CAP_SYS_RAWIO)", + dev_node + ); + dev_node.as_os_str().to_str().map(|s| s.to_string()) + } + }; + if let Some(dev_str) = dev_str { + if let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await { if let DeviceHandle::Scsi(scsi) = dev_type.clone() { let ctrl = ScsiZbus::new(scsi); if ctrl diff --git a/asusd/src/aura_scsi/mod.rs b/asusd/src/aura_scsi/mod.rs index 69e7c7f90..5e77ae47c 100644 --- a/asusd/src/aura_scsi/mod.rs +++ b/asusd/src/aura_scsi/mod.rs @@ -27,7 +27,12 @@ impl ScsiAura { pub async fn write_effect(&self, effect: &AuraEffect) -> Result<(), RogError> { let mut tasks: Vec = effect.into(); for task in &mut tasks { - self.device.lock().await.perform(task).ok(); + // Surface the ioctl errno instead of dropping it — an EPERM/EIO + // here was previously invisible, so asusd reported success while + // no SCSI traffic ever reached the device. + if let Err(e) = self.device.lock().await.perform(task) { + log::warn!("SCSI perform failed: {e}"); + } } Ok(()) }