From 81b84c9316006bb92885d700fd1e9dc01b929184 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 5 Aug 2026 18:35:56 +0800 Subject: [PATCH 1/2] Add --wifisar Linux only right now, but no reason it shouldn't work on windows Signed-off-by: Daniel Schaefer --- EXAMPLES_ADVANCED.md | 44 +++ framework_lib/src/commandline/clap_std.rs | 5 + framework_lib/src/commandline/mod.rs | 9 + framework_lib/src/commandline/uefi.rs | 4 + framework_lib/src/lib.rs | 1 + framework_lib/src/wifi_sar.rs | 365 ++++++++++++++++++ .../completions/bash/framework_tool | 2 +- .../completions/fish/framework_tool.fish | 1 + .../completions/zsh/_framework_tool | 1 + framework_tool/framework_tool.1 | 5 +- 10 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 framework_lib/src/wifi_sar.rs diff --git a/EXAMPLES_ADVANCED.md b/EXAMPLES_ADVANCED.md index c52b296b..593faa98 100644 --- a/EXAMPLES_ADVANCED.md +++ b/EXAMPLES_ADVANCED.md @@ -470,3 +470,47 @@ r12: sp :00000000 lr : pc : cfsr = 0, shcsr = 0, hfsr = 0, dfsr = 0 ``` + + +## WiFi SAR + +``` +> framework_tool --wifisar +WRDS - Wi-Fi SAR limits + Revision: 2 (4 chains, 11 sub-bands) + Mode: 0x00000001 (SAR enabled) + Profile 1 + Subband Range (MHz) ChainA ChainB + 2.4G 2400 0x80 16.0 dBm 0x80 16.0 dBm + 5G2/3 5180-5320 0x70 14.0 dBm 0x70 14.0 dBm + 5G4 5340-5480 0x70 14.0 dBm 0x70 14.0 dBm + 5G6 5500-5720 0x74 14.5 dBm 0x74 14.5 dBm + 5G8/9 5745-5885 0x6C 13.5 dBm 0x6C 13.5 dBm + 6G1 5955-6175 0x6C 13.5 dBm 0x6C 13.5 dBm + 6G3 6195-6415 0x6C 13.5 dBm 0x6C 13.5 dBm + 6G5 6435-6515 0x6C 13.5 dBm 0x6C 13.5 dBm + 6G6 6535-6695 0x6C 13.5 dBm 0x6C 13.5 dBm + 6G8 6715-6855 0x6C 13.5 dBm 0x6C 13.5 dBm + 7G0 6875-7115 0x6C 13.5 dBm 0x6C 13.5 dBm + All zero: CdbChainA, CdbChainB +EWRD - Additional Wi-Fi SAR limits + Revision: 2 (4 chains, 11 sub-bands) + Mode: 0x00000001 (SAR enabled) + Profiles in use: 1 + Profile 2 + Subband Range (MHz) ChainA ChainB + 2.4G 2400 0xA8 21.0 dBm 0xA8 21.0 dBm + 5G2/3 5180-5320 0xA8 21.0 dBm 0xA8 21.0 dBm + 5G4 5340-5480 0xA8 21.0 dBm 0xA8 21.0 dBm + 5G6 5500-5720 0xAC 21.5 dBm 0xAC 21.5 dBm + 5G8/9 5745-5885 0xAC 21.5 dBm 0xAC 21.5 dBm + 6G1 5955-6175 0xAC 21.5 dBm 0xAC 21.5 dBm + 6G3 6195-6415 0xAC 21.5 dBm 0xAC 21.5 dBm + 6G5 6435-6515 0xA8 21.0 dBm 0xA8 21.0 dBm + 6G6 6535-6695 0xA8 21.0 dBm 0xA8 21.0 dBm + 6G8 6715-6855 0xA8 21.0 dBm 0xA8 21.0 dBm + 7G0 6875-7115 0x70 14.0 dBm 0x70 14.0 dBm + All zero: CdbChainA, CdbChainB + Profile 3 (unused): All zero + Profile 4 (unused): All zero +``` diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 615d6bf3..23d7beac 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -325,6 +325,10 @@ struct ClapCli { #[arg(long)] panicinfo: bool, + /// Show Wi-Fi SAR power limits from UEFI variables (WRDS, EWRD) + #[arg(long)] + wifisar: bool, + /// Hash a file of arbitrary data #[arg(long)] hash: Option, @@ -719,6 +723,7 @@ pub fn parse(args: &[String]) -> Cli { switches: args.switches, port80read: args.port80read, panicinfo: args.panicinfo, + wifisar: args.wifisar, hash: args.hash.map(|x| x.into_os_string().into_string().unwrap()), driver: args.driver, pd_addrs, diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index 41477625..ed77e1f0 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -71,6 +71,7 @@ use crate::touchscreen; #[cfg(feature = "rusb")] use crate::usbhub::check_usbhub_version; use crate::util::{self, Config, Platform, PlatformFamily}; +use crate::wifi_sar; use dmidecode::Structure; #[cfg(feature = "hidapi")] use hidapi::HidApi; @@ -256,6 +257,7 @@ pub struct Cli { pub switches: bool, pub port80read: bool, pub panicinfo: bool, + pub wifisar: bool, pub hash: Option, pub pd_addrs: Option<(u16, u16, u16)>, pub pd_ports: Option<(u8, u8, u8)>, @@ -355,6 +357,7 @@ pub fn parse(args: &[String]) -> Cli { switches: cli.switches, port80read: cli.port80read, panicinfo: cli.panicinfo, + wifisar: cli.wifisar, hash: cli.hash, pd_addrs: cli.pd_addrs, pd_ports: cli.pd_ports, @@ -1660,6 +1663,11 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { return 1; } } + } else if args.wifisar { + if wifi_sar::print_wifi_sar().is_none() { + println!("Failed to read Wi-Fi SAR limits"); + return 1; + } } else if args.test { println!("Self-Test"); let result = selftest(&ec); @@ -2086,6 +2094,7 @@ Options: --switches Show current EC switch positions (lid, power button, ...) --port80read Show history of port 80 writes (POST codes) --panicinfo Show saved EC panic info + --wifisar Show Wi-Fi SAR power limits from UEFI variables (WRDS, EWRD) --intrusion Show status of intrusion switch --inputdeck Show status of the input deck --inputdeck-mode Set input deck power mode [possible values: auto, off, on] (Laptop 12, 13, 16) diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index 929673d6..bb95fd06 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -99,6 +99,7 @@ pub fn parse(args: &[String]) -> Cli { switches: false, port80read: false, panicinfo: false, + wifisar: false, hash: None, // This is the only driver that works on UEFI driver: Some(CrosEcDriverType::Portio), @@ -593,6 +594,9 @@ pub fn parse(args: &[String]) -> Cli { } else if arg == "--panicinfo" { cli.panicinfo = true; found_an_option = true; + } else if arg == "--wifisar" { + cli.wifisar = true; + found_an_option = true; } else if arg == "-t" || arg == "--test" { cli.test = true; found_an_option = true; diff --git a/framework_lib/src/lib.rs b/framework_lib/src/lib.rs index 64d6cb5f..6e85fa2c 100644 --- a/framework_lib/src/lib.rs +++ b/framework_lib/src/lib.rs @@ -65,6 +65,7 @@ pub mod power; pub mod smart_battery; pub mod smbios; mod util; +pub mod wifi_sar; pub mod built_info { // The file has been placed there by the build script. diff --git a/framework_lib/src/wifi_sar.rs b/framework_lib/src/wifi_sar.rs new file mode 100644 index 00000000..283a6dd9 --- /dev/null +++ b/framework_lib/src/wifi_sar.rs @@ -0,0 +1,365 @@ +//! Get the Intel CNVi Wi-Fi SAR power tables from UEFI variables. +//! +//! Currently only implemented on Linux (needs root) and UEFI. +//! +//! SAR (Specific Absorption Rate) limits are the maximum TX power the Wi-Fi +//! module may use per frequency sub-band. The BIOS hands them to the driver in +//! UEFI variables. WRDS holds SAR profile 1, EWRD the remaining profiles. +//! +//! The layouts are the same as in the Linux iwlwifi driver, see +//! `drivers/net/wireless/intel/iwlwifi/fw/uefi.h` (struct uefi_cnv_var_wrds and +//! uefi_cnv_var_ewrd). + +#[allow(unused_imports)] +use log::{debug, error, info, trace}; +use std::prelude::v1::*; + +#[cfg(all(not(feature = "uefi"), target_os = "linux"))] +use std::fs; + +#[cfg(feature = "uefi")] +use uefi::runtime::{self, VariableVendor}; +#[cfg(feature = "uefi")] +use uefi::{guid, CStr16}; + +/// Vendor GUID of the Intel CNVi Wi-Fi UEFI variables +pub const CNVI_WIFI_GUID: &str = "92daaf2f-c02b-455b-b2ec-f5a3594f4aea"; +#[cfg(feature = "uefi")] +const CNVI_WIFI_VENDOR: VariableVendor = + VariableVendor(guid!("92daaf2f-c02b-455b-b2ec-f5a3594f4aea")); + +const WRDS_VAR: &str = "UefiCnvWlanWRDS"; +const EWRD_VAR: &str = "UefiCnvWlanEWRD"; + +/// UEFI_SAR_MAX_CHAINS_PER_PROFILE +const CHAINS: usize = 4; +const CHAIN_NAMES: [&str; CHAINS] = ["ChainA", "ChainB", "CdbChainA", "CdbChainB"]; +/// BIOS_SAR_MAX_PROFILE_NUM +const MAX_PROFILES: usize = 4; +/// IWL_SAR_ENABLE_MSK +const SAR_ENABLE: u32 = 1; + +/// Vendor names and frequency ranges of the revision 2 sub-bands. +/// Revision 3 adds a 12th sub-band that neither the driver nor the vendor names. +const SUB_BANDS: [(&str, &str); 11] = [ + ("2.4G", "2400"), + ("5G2/3", "5180-5320"), + ("5G4", "5340-5480"), + ("5G6", "5500-5720"), + ("5G8/9", "5745-5885"), + ("6G1", "5955-6175"), + ("6G3", "6195-6415"), + ("6G5", "6435-6515"), + ("6G6", "6535-6695"), + ("6G8", "6715-6855"), + ("7G0", "6875-7115"), +]; + +/// UEFI_SAR_SUB_BANDS_NUM_REV2 and _REV3 +fn sub_bands(revision: u8) -> Option { + match revision { + 2 => Some(11), + 3 => Some(12), + _ => None, + } +} + +/// Names and frequency ranges of the sub-bands of a table with `n` of them +fn sub_band_names(n: usize) -> impl Iterator { + (0..n).map(|i| *SUB_BANDS.get(i).unwrap_or(&("?", "?"))) +} + +#[derive(Debug)] +pub struct SarTable { + pub revision: u8, + /// Bit 0 tells the driver whether to apply SAR at all + pub mode: u32, + /// How many of the profiles the BIOS actually filled in. Only in EWRD. + pub num_profiles: Option, + /// Number of the first profile in `profiles`. 1 for WRDS, 2 for EWRD. + pub first_profile: usize, + /// Power limits, indexed by profile, chain and sub-band, in 1/8 dBm + pub profiles: Vec>>, +} + +impl SarTable { + fn is_used(&self, profile: usize) -> bool { + match self.num_profiles { + // WRDS' single profile is always in use + None => true, + Some(num) => (profile - self.first_profile) < num as usize, + } + } +} + +/// Split the power limits of `profiles` profiles into profiles, chains and sub-bands +fn parse_profiles(data: &[u8], profiles: usize, sub_bands: usize) -> Vec>> { + let per_profile = CHAINS * sub_bands; + (0..profiles) + .map(|p| { + (0..CHAINS) + .map(|c| { + let start = p * per_profile + c * sub_bands; + data[start..start + sub_bands].to_vec() + }) + .collect() + }) + .collect() +} + +/// Parse WRDS, which holds SAR profile 1 +/// +/// struct uefi_cnv_var_wrds: u8 revision, u32 mode, then one profile +pub fn parse_wrds(data: &[u8]) -> Option { + let revision = *data.first()?; + let sub_bands = sub_bands(revision).or_else(|| { + error!("WRDS: Unsupported revision {}", revision); + None + })?; + let expected = 5 + CHAINS * sub_bands; + if data.len() != expected { + error!( + "WRDS: Revision {} should be {} bytes, got {}", + revision, + expected, + data.len() + ); + return None; + } + + Some(SarTable { + revision, + mode: u32::from_le_bytes(data[1..5].try_into().unwrap()), + num_profiles: None, + first_profile: 1, + profiles: parse_profiles(&data[5..], 1, sub_bands), + }) +} + +/// Parse EWRD, which holds SAR profiles 2 and up +/// +/// struct uefi_cnv_var_ewrd: u8 revision, u32 mode, u32 num_profiles, +/// then MAX_PROFILES-1 profiles +pub fn parse_ewrd(data: &[u8]) -> Option { + let revision = *data.first()?; + let sub_bands = sub_bands(revision).or_else(|| { + error!("EWRD: Unsupported revision {}", revision); + None + })?; + let profiles = MAX_PROFILES - 1; + let expected = 9 + CHAINS * sub_bands * profiles; + if data.len() != expected { + error!( + "EWRD: Revision {} should be {} bytes, got {}", + revision, + expected, + data.len() + ); + return None; + } + + let num_profiles = u32::from_le_bytes(data[5..9].try_into().unwrap()); + if num_profiles as usize > profiles { + error!("EWRD: Invalid number of profiles: {}", num_profiles); + return None; + } + + Some(SarTable { + revision, + mode: u32::from_le_bytes(data[1..5].try_into().unwrap()), + num_profiles: Some(num_profiles), + first_profile: 2, + profiles: parse_profiles(&data[9..], profiles, sub_bands), + }) +} + +/// Format a power limit of 1/8 dBm units as dBm +fn dbm(limit: u8) -> String { + format!("{:.1}", f32::from(limit) / 8.0) +} + +pub fn print_sar_table(name: &str, table: &SarTable) { + let sub_bands = table.profiles[0][0].len(); + println!("{}", name); + println!( + " Revision: {} ({} chains, {} sub-bands)", + table.revision, CHAINS, sub_bands + ); + println!( + " Mode: 0x{:08X} (SAR {})", + table.mode, + if table.mode & SAR_ENABLE != 0 { + "enabled" + } else { + "disabled" + } + ); + if let Some(num_profiles) = table.num_profiles { + println!(" Profiles in use: {}", num_profiles); + } + + for (i, chains) in table.profiles.iter().enumerate() { + let profile = table.first_profile + i; + let unused = if table.is_used(profile) { + "" + } else { + " (unused)" + }; + // Most systems only populate ChainA and ChainB + let used: Vec = (0..CHAINS) + .filter(|c| chains[*c].iter().any(|limit| *limit != 0)) + .collect(); + if used.is_empty() { + println!(" Profile {}{}: All zero", profile, unused); + continue; + } + + println!(" Profile {}{}", profile, unused); + print!(" {:<7} {:<11}", "Subband", "Range (MHz)"); + for c in &used { + print!(" {:<13}", CHAIN_NAMES[*c]); + } + println!(); + for (s, (name, range)) in sub_band_names(sub_bands).enumerate() { + print!(" {:<7} {:<11}", name, range); + for c in &used { + print!(" 0x{:02X} {:>4} dBm", chains[*c][s], dbm(chains[*c][s])); + } + println!(); + } + let zero: Vec<&str> = (0..CHAINS) + .filter(|c| !used.contains(c)) + .map(|c| CHAIN_NAMES[c]) + .collect(); + if !zero.is_empty() { + println!(" All zero: {}", zero.join(", ")); + } + } +} + +pub fn print_wifi_sar() -> Option<()> { + let mut found = false; + + if let Some(data) = get_variable(WRDS_VAR) { + if let Some(table) = parse_wrds(&data) { + print_sar_table("WRDS - Wi-Fi SAR limits", &table); + found = true; + } + } + if let Some(data) = get_variable(EWRD_VAR) { + if let Some(table) = parse_ewrd(&data) { + print_sar_table("EWRD - Additional Wi-Fi SAR limits", &table); + found = true; + } + } + + if found { + Some(()) + } else { + None + } +} + +/// Read a UEFI variable of the Intel CNVi Wi-Fi vendor GUID +#[cfg(all(not(feature = "uefi"), target_os = "linux"))] +fn get_variable(name: &str) -> Option> { + let path = format!("/sys/firmware/efi/efivars/{}-{}", name, CNVI_WIFI_GUID); + let data = fs::read(&path) + .map_err(|err| { + error!("Failed to read {}: {}", path, err); + info!("Make sure you're root to access UEFI variables from sysfs on Linux"); + }) + .ok()?; + // In sysfs the payload is prefixed by the 32bit variable attributes + if data.len() < 4 { + error!("{} is too short: {} bytes", path, data.len()); + return None; + } + Some(data[4..].to_vec()) +} + +#[cfg(feature = "uefi")] +fn get_variable(name: &str) -> Option> { + let mut buf = [0; 32]; + let name = CStr16::from_str_with_buf(name, &mut buf) + .map_err(|err| error!("Invalid variable name {}: {:?}", name, err)) + .ok()?; + runtime::get_variable_boxed(name, &CNVI_WIFI_VENDOR) + .map_err(|err| error!("Failed to read UEFI variable {}: {:?}", name, err)) + .ok() + .map(|(data, _attributes)| data.to_vec()) +} + +#[cfg(all(not(feature = "uefi"), not(target_os = "linux")))] +fn get_variable(_name: &str) -> Option> { + error!("Reading UEFI variables is not implemented on this OS"); + None +} + +#[cfg(test)] +mod tests { + use super::*; + + /// WRDS and EWRD as dumped from a Framework Laptop 13 (Intel Core Ultra 1) + const WRDS: &[u8] = &[ + 0x02, 0x01, 0x00, 0x00, 0x00, 0x80, 0x70, 0x70, 0x74, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, + 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + #[test] + fn parse_wrds_table() { + // ChainB holds the same limits as ChainA, CdbChain{A,B} are unused + let mut data = WRDS.to_vec(); + data[16..27].copy_from_slice(&WRDS[5..16]); + + let table = parse_wrds(&data).unwrap(); + assert_eq!(table.revision, 2); + assert_eq!(table.mode, SAR_ENABLE); + assert_eq!(table.num_profiles, None); + assert_eq!(table.first_profile, 1); + assert_eq!(table.profiles.len(), 1); + // 0x80 = 16 dBm in 1/8 dBm units + assert_eq!(table.profiles[0][0][0], 0x80); + assert_eq!(dbm(table.profiles[0][0][0]), "16.0"); + assert_eq!(dbm(table.profiles[0][1][10]), "13.5"); + assert_eq!(table.profiles[0][2], [0; 11]); + assert_eq!(table.profiles[0][3], [0; 11]); + } + + #[test] + fn parse_wrds_rejects_bad_table() { + // Unsupported revision + let mut data = WRDS.to_vec(); + data[0] = 1; + assert!(parse_wrds(&data).is_none()); + + // Truncated + assert!(parse_wrds(&WRDS[..WRDS.len() - 1]).is_none()); + assert!(parse_wrds(&[]).is_none()); + } + + #[test] + fn parse_ewrd_table() { + // Revision 2, SAR enabled, one extra profile + let mut data = vec![0x02, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]; + data.resize(9 + CHAINS * 11 * (MAX_PROFILES - 1), 0); + data[9] = 0xa8; + + let table = parse_ewrd(&data).unwrap(); + assert_eq!(table.revision, 2); + assert_eq!(table.num_profiles, Some(1)); + assert_eq!(table.first_profile, 2); + assert_eq!(table.profiles.len(), MAX_PROFILES - 1); + assert_eq!(dbm(table.profiles[0][0][0]), "21.0"); + // Only profile 2 is in use, 3 and 4 are beyond num_profiles + assert!(table.is_used(2)); + assert!(!table.is_used(3)); + assert!(!table.is_used(4)); + + // More profiles than the firmware can hold + data[5] = 0x04; + assert!(parse_ewrd(&data).is_none()); + } +} diff --git a/framework_tool/completions/bash/framework_tool b/framework_tool/completions/bash/framework_tool index 518b31ab..74fad078 100755 --- a/framework_tool/completions/bash/framework_tool +++ b/framework_tool/completions/bash/framework_tool @@ -23,7 +23,7 @@ _framework_tool() { case "${cmd}" in framework_tool) - opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --smartbattery --smartbattery-auth --thermal --thermalget --thermalset --sensors --fansetduty --fansetrpm --autofanctrl --pdports --pdports-chromebook --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-full-ec --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --haptic-intensity --click-force --stylus-battery --console --reboot-ec --ec-hib-delay --sysinfo --uptimeinfo --s0ix-counter --hello --protoinfo --switches --port80read --panicinfo --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --validate-gpu-descriptor-file --nvidia --host-command --generate-completions --generate-manpage --help" + opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --smartbattery --smartbattery-auth --thermal --thermalget --thermalset --sensors --fansetduty --fansetrpm --autofanctrl --pdports --pdports-chromebook --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-full-ec --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --haptic-intensity --click-force --stylus-battery --console --reboot-ec --ec-hib-delay --sysinfo --uptimeinfo --s0ix-counter --hello --protoinfo --switches --port80read --panicinfo --wifisar --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --validate-gpu-descriptor-file --nvidia --host-command --generate-completions --generate-manpage --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/framework_tool/completions/fish/framework_tool.fish b/framework_tool/completions/fish/framework_tool.fish index b46a6cb0..b06022c6 100644 --- a/framework_tool/completions/fish/framework_tool.fish +++ b/framework_tool/completions/fish/framework_tool.fish @@ -113,6 +113,7 @@ complete -c framework_tool -l protoinfo -d 'Show EC host command protocol info' complete -c framework_tool -l switches -d 'Show current EC switch positions (lid, power button, ...)' complete -c framework_tool -l port80read -d 'Show history of port 80 writes (POST codes)' complete -c framework_tool -l panicinfo -d 'Show saved EC panic info' +complete -c framework_tool -l wifisar -d 'Show Wi-Fi SAR power limits from UEFI variables (WRDS, EWRD)' complete -c framework_tool -s t -l test -d 'Run self-test to check if interaction with EC is possible' complete -c framework_tool -l test-retimer -d 'Run self-test to check if interaction with retimers is possible' complete -c framework_tool -l boardid -d 'Print all board IDs' diff --git a/framework_tool/completions/zsh/_framework_tool b/framework_tool/completions/zsh/_framework_tool index 1495920b..15e112fc 100644 --- a/framework_tool/completions/zsh/_framework_tool +++ b/framework_tool/completions/zsh/_framework_tool @@ -97,6 +97,7 @@ _framework_tool() { '--switches[Show current EC switch positions (lid, power button, ...)]' \ '--port80read[Show history of port 80 writes (POST codes)]' \ '--panicinfo[Show saved EC panic info]' \ +'--wifisar[Show Wi-Fi SAR power limits from UEFI variables (WRDS, EWRD)]' \ '-t[Run self-test to check if interaction with EC is possible]' \ '--test[Run self-test to check if interaction with EC is possible]' \ '--test-retimer[Run self-test to check if interaction with retimers is possible]' \ diff --git a/framework_tool/framework_tool.1 b/framework_tool/framework_tool.1 index aaf135d5..f724e837 100644 --- a/framework_tool/framework_tool.1 +++ b/framework_tool/framework_tool.1 @@ -8,7 +8,7 @@ framework_tool \- Swiss army knife for Framework laptops .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH SYNOPSIS -\fBframework_tool\fR [\fB\-\-flash\-gpu\-descriptor\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-q\fR|\fB\-\-quiet\fR]... [\fB\-\-versions\fR] [\fB\-\-version\fR] [\fB\-\-features\fR] [\fB\-\-esrt\fR] [\fB\-\-device\fR] [\fB\-\-compare\-version\fR] [\fB\-\-power\fR] [\fB\-\-smartbattery\fR] [\fB\-\-smartbattery\-auth\fR] [\fB\-\-thermal\fR] [\fB\-\-thermalget\fR] [\fB\-\-thermalset\fR] [\fB\-\-sensors\fR] [\fB\-\-fansetduty\fR] [\fB\-\-fansetrpm\fR] [\fB\-\-autofanctrl\fR] [\fB\-\-pdports\fR] [\fB\-\-pdports\-chromebook\fR] [\fB\-\-info\fR] [\fB\-\-meinfo\fR] [\fB\-\-pd\-info\fR] [\fB\-\-pd\-reset\fR] [\fB\-\-pd\-disable\fR] [\fB\-\-pd\-enable\fR] [\fB\-\-dp\-hdmi\-info\fR] [\fB\-\-dp\-hdmi\-update\fR] [\fB\-\-audio\-card\-info\fR] [\fB\-\-privacy\fR] [\fB\-\-pd\-bin\fR] [\fB\-\-ec\-bin\fR] [\fB\-\-capsule\fR] [\fB\-\-dump\fR] [\fB\-\-h2o\-capsule\fR] [\fB\-\-dump\-ec\-flash\fR] [\fB\-\-flash\-full\-ec\fR] [\fB\-\-flash\-ec\fR] [\fB\-\-flash\-ro\-ec\fR] [\fB\-\-flash\-rw\-ec\fR] [\fB\-\-intrusion\fR] [\fB\-\-inputdeck\fR] [\fB\-\-inputdeck\-mode\fR] [\fB\-\-expansion\-bay\fR] [\fB\-\-charge\-limit\fR] [\fB\-\-charge\-current\-limit\fR] [\fB\-\-charge\-rate\-limit\fR] [\fB\-\-get\-gpio\fR] [\fB\-\-fp\-led\-level\fR] [\fB\-\-fp\-brightness\fR] [\fB\-\-kblight\fR] [\fB\-\-remap\-key\fR] [\fB\-\-rgbkbd\fR] [\fB\-\-tablet\-mode\fR] [\fB\-\-touchscreen\-enable\fR] [\fB\-\-haptic\-intensity\fR] [\fB\-\-click\-force\fR] [\fB\-\-stylus\-battery\fR] [\fB\-\-console\fR] [\fB\-\-reboot\-ec\fR] [\fB\-\-ec\-hib\-delay\fR] [\fB\-\-sysinfo\fR] [\fB\-\-uptimeinfo\fR] [\fB\-\-s0ix\-counter\fR] [\fB\-\-hello\fR] [\fB\-\-protoinfo\fR] [\fB\-\-switches\fR] [\fB\-\-port80read\fR] [\fB\-\-panicinfo\fR] [\fB\-\-hash\fR] [\fB\-\-driver\fR] [\fB\-\-pd\-addrs\fR] [\fB\-\-pd\-ports\fR] [\fB\-t\fR|\fB\-\-test\fR] [\fB\-\-test\-retimer\fR] [\fB\-\-boardid\fR] [\fB\-f\fR|\fB\-\-force\fR] [\fB\-\-dry\-run\fR] [\fB\-\-flash\-gpu\-descriptor\-file\fR] [\fB\-\-dump\-gpu\-descriptor\-file\fR] [\fB\-\-validate\-gpu\-descriptor\-file\fR] [\fB\-\-nvidia\fR] [\fB\-\-host\-command\fR] [\fB\-h\fR|\fB\-\-help\fR] +\fBframework_tool\fR [\fB\-\-flash\-gpu\-descriptor\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-q\fR|\fB\-\-quiet\fR]... [\fB\-\-versions\fR] [\fB\-\-version\fR] [\fB\-\-features\fR] [\fB\-\-esrt\fR] [\fB\-\-device\fR] [\fB\-\-compare\-version\fR] [\fB\-\-power\fR] [\fB\-\-smartbattery\fR] [\fB\-\-smartbattery\-auth\fR] [\fB\-\-thermal\fR] [\fB\-\-thermalget\fR] [\fB\-\-thermalset\fR] [\fB\-\-sensors\fR] [\fB\-\-fansetduty\fR] [\fB\-\-fansetrpm\fR] [\fB\-\-autofanctrl\fR] [\fB\-\-pdports\fR] [\fB\-\-pdports\-chromebook\fR] [\fB\-\-info\fR] [\fB\-\-meinfo\fR] [\fB\-\-pd\-info\fR] [\fB\-\-pd\-reset\fR] [\fB\-\-pd\-disable\fR] [\fB\-\-pd\-enable\fR] [\fB\-\-dp\-hdmi\-info\fR] [\fB\-\-dp\-hdmi\-update\fR] [\fB\-\-audio\-card\-info\fR] [\fB\-\-privacy\fR] [\fB\-\-pd\-bin\fR] [\fB\-\-ec\-bin\fR] [\fB\-\-capsule\fR] [\fB\-\-dump\fR] [\fB\-\-h2o\-capsule\fR] [\fB\-\-dump\-ec\-flash\fR] [\fB\-\-flash\-full\-ec\fR] [\fB\-\-flash\-ec\fR] [\fB\-\-flash\-ro\-ec\fR] [\fB\-\-flash\-rw\-ec\fR] [\fB\-\-intrusion\fR] [\fB\-\-inputdeck\fR] [\fB\-\-inputdeck\-mode\fR] [\fB\-\-expansion\-bay\fR] [\fB\-\-charge\-limit\fR] [\fB\-\-charge\-current\-limit\fR] [\fB\-\-charge\-rate\-limit\fR] [\fB\-\-get\-gpio\fR] [\fB\-\-fp\-led\-level\fR] [\fB\-\-fp\-brightness\fR] [\fB\-\-kblight\fR] [\fB\-\-remap\-key\fR] [\fB\-\-rgbkbd\fR] [\fB\-\-tablet\-mode\fR] [\fB\-\-touchscreen\-enable\fR] [\fB\-\-haptic\-intensity\fR] [\fB\-\-click\-force\fR] [\fB\-\-stylus\-battery\fR] [\fB\-\-console\fR] [\fB\-\-reboot\-ec\fR] [\fB\-\-ec\-hib\-delay\fR] [\fB\-\-sysinfo\fR] [\fB\-\-uptimeinfo\fR] [\fB\-\-s0ix\-counter\fR] [\fB\-\-hello\fR] [\fB\-\-protoinfo\fR] [\fB\-\-switches\fR] [\fB\-\-port80read\fR] [\fB\-\-panicinfo\fR] [\fB\-\-wifisar\fR] [\fB\-\-hash\fR] [\fB\-\-driver\fR] [\fB\-\-pd\-addrs\fR] [\fB\-\-pd\-ports\fR] [\fB\-t\fR|\fB\-\-test\fR] [\fB\-\-test\-retimer\fR] [\fB\-\-boardid\fR] [\fB\-f\fR|\fB\-\-force\fR] [\fB\-\-dry\-run\fR] [\fB\-\-flash\-gpu\-descriptor\-file\fR] [\fB\-\-dump\-gpu\-descriptor\-file\fR] [\fB\-\-validate\-gpu\-descriptor\-file\fR] [\fB\-\-nvidia\fR] [\fB\-\-host\-command\fR] [\fB\-h\fR|\fB\-\-help\fR] .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH DESCRIPTION @@ -359,6 +359,9 @@ Show history of port 80 writes (POST codes) \fB\-\-panicinfo\fR Show saved EC panic info .TP +\fB\-\-wifisar\fR +Show Wi\-Fi SAR power limits from UEFI variables (WRDS, EWRD) +.TP \fB\-\-hash\fR \fI\fR Hash a file of arbitrary data .TP From cbc82e34565e583d2d759f22e564e5249acc0f8a Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 5 Aug 2026 20:25:27 +0800 Subject: [PATCH 2/2] --wifisar: windows support Signed-off-by: Daniel Schaefer --- framework_lib/Cargo.toml | 4 +- framework_lib/src/wifi_sar.rs | 120 +++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/framework_lib/Cargo.toml b/framework_lib/Cargo.toml index 512d7b32..8dcbc37b 100644 --- a/framework_lib/Cargo.toml +++ b/framework_lib/Cargo.toml @@ -26,7 +26,7 @@ built = { version = "0.8", features = ["chrono", "git2"] } [dependencies] lazy_static = "1.4.0" dmidecode = { version = "1", default-features = false } -sha1 = { version = "0.10.6", default-features = false } +sha1 = { version = "0.10.6", default-features = false, features = [ "force-soft" ] } sha2 = { version = "0.10.8", default-features = false, features = [ "force-soft" ] } regex = { version = "1.11.1", default-features = false } num = { version = "0.4", default-features = false } @@ -80,6 +80,8 @@ features = [ "Win32_System_Ioctl", "Win32_System_SystemInformation", "Win32_System_SystemServices", + # For reading UEFI variables + "Win32_System_WindowsProgramming", # For HID devices "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_HumanInterfaceDevice", diff --git a/framework_lib/src/wifi_sar.rs b/framework_lib/src/wifi_sar.rs index 283a6dd9..310e333a 100644 --- a/framework_lib/src/wifi_sar.rs +++ b/framework_lib/src/wifi_sar.rs @@ -1,6 +1,7 @@ //! Get the Intel CNVi Wi-Fi SAR power tables from UEFI variables. //! -//! Currently only implemented on Linux (needs root) and UEFI. +//! Currently only implemented on Linux (needs root), Windows (needs +//! Administrator) and UEFI. //! //! SAR (Specific Absorption Rate) limits are the maximum TX power the Wi-Fi //! module may use per frequency sub-band. The BIOS hands them to the driver in @@ -17,6 +18,25 @@ use std::prelude::v1::*; #[cfg(all(not(feature = "uefi"), target_os = "linux"))] use std::fs; +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +use core::ffi::c_void; +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +use windows::core::PCWSTR; +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +use windows::Win32::Foundation::{ + CloseHandle, GetLastError, ERROR_ENVVAR_NOT_FOUND, ERROR_INVALID_FUNCTION, + ERROR_NOT_ALL_ASSIGNED, ERROR_PRIVILEGE_NOT_HELD, HANDLE, LUID, +}; +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +use windows::Win32::Security::{ + AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES, SE_PRIVILEGE_ENABLED, + SE_SYSTEM_ENVIRONMENT_NAME, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES, +}; +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +use windows::Win32::System::WindowsProgramming::GetFirmwareEnvironmentVariableW; + #[cfg(feature = "uefi")] use uefi::runtime::{self, VariableVendor}; #[cfg(feature = "uefi")] @@ -290,7 +310,103 @@ fn get_variable(name: &str) -> Option> { .map(|(data, _attributes)| data.to_vec()) } -#[cfg(all(not(feature = "uefi"), not(target_os = "linux")))] +/// Enable SeSystemEnvironmentPrivilege, which is required to read UEFI variables +/// +/// Elevated processes have the privilege in their token, but it's disabled by +/// default, so we have to enable it ourselves. +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +fn enable_system_environment_privilege() -> bool { + let mut token = HANDLE::default(); + // SAFETY: Both handles are only used within this function and the token is + // closed before returning + unsafe { + if let Err(err) = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &mut token) + { + error!("Failed to open process token: {}", err); + return false; + } + + let mut luid = LUID::default(); + let res = LookupPrivilegeValueW(PCWSTR::null(), SE_SYSTEM_ENVIRONMENT_NAME, &mut luid) + .and_then(|_| { + let privileges = TOKEN_PRIVILEGES { + PrivilegeCount: 1, + Privileges: [LUID_AND_ATTRIBUTES { + Luid: luid, + Attributes: SE_PRIVILEGE_ENABLED, + }], + }; + AdjustTokenPrivileges(token, false, Some(&privileges), 0, None, None) + }); + // AdjustTokenPrivileges succeeds even if it couldn't assign the + // privilege, that's only reported by the last error + let assigned = GetLastError() != ERROR_NOT_ALL_ASSIGNED; + let _ = CloseHandle(token); + + if let Err(err) = res { + error!("Failed to enable SeSystemEnvironmentPrivilege: {}", err); + return false; + } + if !assigned { + error!("Not allowed to enable SeSystemEnvironmentPrivilege"); + info!("Make sure to run as Administrator to access UEFI variables on Windows"); + return false; + } + } + true +} + +/// Encode a string as a NUL terminated wide string for the Win32 API +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +fn wide(s: &str) -> Vec { + s.encode_utf16().chain(core::iter::once(0)).collect() +} + +/// Read a UEFI variable of the Intel CNVi Wi-Fi vendor GUID +#[cfg(all(not(feature = "uefi"), target_os = "windows"))] +fn get_variable(name: &str) -> Option> { + if !enable_system_environment_privilege() { + return None; + } + + let name_wide = wide(name); + // Win32 wants the vendor GUID as a string in braces + let guid_wide = wide(&format!("{{{}}}", CNVI_WIFI_GUID)); + // Plenty for all revisions of the tables we know about + let mut buf = [0; 1024]; + // SAFETY: Both strings are NUL terminated and the buffer length is passed along + let len = unsafe { + GetFirmwareEnvironmentVariableW( + PCWSTR(name_wide.as_ptr()), + PCWSTR(guid_wide.as_ptr()), + Some(buf.as_mut_ptr() as *mut c_void), + buf.len() as u32, + ) + }; + if len == 0 { + // SAFETY: Called right after the failed call above + match unsafe { GetLastError() } { + ERROR_INVALID_FUNCTION => { + error!("Failed to read UEFI variable {}", name); + info!("UEFI variables are only available when booted in UEFI mode"); + } + ERROR_PRIVILEGE_NOT_HELD => { + error!("Not allowed to read UEFI variable {}", name); + info!("Make sure to run as Administrator to access UEFI variables on Windows"); + } + ERROR_ENVVAR_NOT_FOUND => error!("UEFI variable {} does not exist", name), + err => error!("Failed to read UEFI variable {}: {:?}", name, err), + } + return None; + } + Some(buf[..len as usize].to_vec()) +} + +#[cfg(all( + not(feature = "uefi"), + not(target_os = "linux"), + not(target_os = "windows") +))] fn get_variable(_name: &str) -> Option> { error!("Reading UEFI variables is not implemented on this OS"); None