diff --git a/completions/bash/framework_tool b/completions/bash/framework_tool index ade3813..ba899a6 100755 --- a/completions/bash/framework_tool +++ b/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 --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --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-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 --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --nvidia --host-command --generate-completions --help" + opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --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-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 --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --list-uefi-vars --get-uefi-var --set-uefi-var --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --nvidia --host-command --generate-completions --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -173,6 +173,14 @@ _framework_tool() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --get-uefi-var) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --set-uefi-var) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; --hash) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/completions/fish/framework_tool.fish b/completions/fish/framework_tool.fish index 00a70a9..8124e34 100644 --- a/completions/fish/framework_tool.fish +++ b/completions/fish/framework_tool.fish @@ -56,6 +56,8 @@ jump-rw\t'' cancel-jump\t'' disable-jump\t''" complete -c framework_tool -l ec-hib-delay -d 'Get or set EC hibernate delay (S5 to G3)' -r +complete -c framework_tool -l get-uefi-var -d 'Get a UEFI variable by name and optionally GUID (GUID auto-resolved for well-known names)' -r +complete -c framework_tool -l set-uefi-var -d 'Set a UEFI variable from file (NAME [GUID] FILEPATH, GUID auto-resolved for well-known names)' -r complete -c framework_tool -l hash -d 'Hash a file of arbitrary data' -r -F complete -c framework_tool -l driver -d 'Select which driver is used. By default portio is used' -r -f -a "portio\t'' cros-ec\t'' @@ -91,6 +93,7 @@ complete -c framework_tool -l expansion-bay -d 'Show status of the expansion bay complete -c framework_tool -l stylus-battery -d 'Check stylus battery level (USI 2.0 stylus only)' complete -c framework_tool -l uptimeinfo complete -c framework_tool -l s0ix-counter +complete -c framework_tool -l list-uefi-vars -d 'List all UEFI variables' 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/completions/zsh/_framework_tool b/completions/zsh/_framework_tool index 3b80dcd..b2dac46 100644 --- a/completions/zsh/_framework_tool +++ b/completions/zsh/_framework_tool @@ -51,6 +51,8 @@ _framework_tool() { '--console=[Get EC console, choose whether recent or to follow the output]:CONSOLE:(recent follow)' \ '--reboot-ec=[Control EC RO/RW jump]:REBOOT_EC:(reboot jump-ro jump-rw cancel-jump disable-jump)' \ '--ec-hib-delay=[Get or set EC hibernate delay (S5 to G3)]::EC_HIB_DELAY:_default' \ +'*--get-uefi-var=[Get a UEFI variable by name and optionally GUID (GUID auto-resolved for well-known names)]:NAME:_default' \ +'*--set-uefi-var=[Set a UEFI variable from file (NAME \[GUID\] FILEPATH, GUID auto-resolved for well-known names)]:NAME:_default:NAME:_default' \ '--hash=[Hash a file of arbitrary data]:HASH:_files' \ '--driver=[Select which driver is used. By default portio is used]:DRIVER:(portio cros-ec windows)' \ '*--pd-addrs=[Specify I2C addresses of the PD chips (Advanced)]:PD_ADDRS:_default:PD_ADDRS:_default:PD_ADDRS:_default' \ @@ -82,6 +84,7 @@ _framework_tool() { '--stylus-battery[Check stylus battery level (USI 2.0 stylus only)]' \ '--uptimeinfo[]' \ '--s0ix-counter[]' \ +'--list-uefi-vars[List all UEFI variables]' \ '-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_lib/Cargo.toml b/framework_lib/Cargo.toml index 8c1e322..a6016aa 100644 --- a/framework_lib/Cargo.toml +++ b/framework_lib/Cargo.toml @@ -73,6 +73,7 @@ features = [ "Win32_System_Ioctl", "Win32_System_SystemInformation", "Win32_System_SystemServices", + "Win32_System_WindowsProgramming", # For HID devices "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_HumanInterfaceDevice", diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index ba1cc43..ad022b9 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -254,6 +254,20 @@ struct ClapCli { #[arg(long)] s0ix_counter: bool, + /// List all UEFI variables + #[arg(long)] + list_uefi_vars: bool, + + /// Get a UEFI variable by name and optionally GUID (GUID auto-resolved for well-known names) + #[clap(num_args = 1..=2)] + #[arg(long, value_names(["NAME", "GUID"]))] + get_uefi_var: Vec, + + /// Set a UEFI variable from file (NAME [GUID] FILEPATH, GUID auto-resolved for well-known names) + #[clap(num_args = 2..=3)] + #[arg(long, value_names(["NAME", "GUID", "FILEPATH"]))] + set_uefi_var: Vec, + /// Hash a file of arbitrary data #[arg(long)] hash: Option, @@ -424,6 +438,53 @@ pub fn parse(args: &[String]) -> Cli { )), _ => None, }; + let get_uefi_var = match args.get_uefi_var.len() { + 2 => Some((args.get_uefi_var[0].clone(), args.get_uefi_var[1].clone())), + 1 => { + let name = &args.get_uefi_var[0]; + if let Some(guid) = crate::os_specific::known_uefi_guid(name) { + Some((name.clone(), guid.to_string())) + } else { + cli.error( + ErrorKind::MissingRequiredArgument, + format!( + "Unknown UEFI variable '{}'. Please provide the GUID explicitly.", + name + ), + ) + .exit(); + } + } + 0 => None, + // Checked by clap + _ => unreachable!(), + }; + let set_uefi_var = match args.set_uefi_var.len() { + 3 => Some(( + args.set_uefi_var[0].clone(), + args.set_uefi_var[1].clone(), + args.set_uefi_var[2].clone(), + )), + 2 => { + let name = &args.set_uefi_var[0]; + if let Some(guid) = crate::os_specific::known_uefi_guid(name) { + Some((name.clone(), guid.to_string(), args.set_uefi_var[1].clone())) + } else { + cli.error( + ErrorKind::MissingRequiredArgument, + format!( + "Unknown UEFI variable '{}'. Please provide the GUID explicitly.", + name + ), + ) + .exit(); + } + } + 0 => None, + // Checked by clap + _ => unreachable!(), + }; + let host_command = if args.host_command.len() >= 2 { let cmd_ver = if let Ok(cmd_ver) = u8::try_from(args.host_command[1]) { cmd_ver @@ -528,6 +589,9 @@ pub fn parse(args: &[String]) -> Cli { ec_hib_delay: args.ec_hib_delay, uptimeinfo: args.uptimeinfo, s0ix_counter: args.s0ix_counter, + list_uefi_vars: args.list_uefi_vars, + get_uefi_var, + set_uefi_var, 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 4ca9af4..20f202d 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -216,6 +216,9 @@ pub struct Cli { pub ec_hib_delay: Option>, pub uptimeinfo: bool, pub s0ix_counter: bool, + pub list_uefi_vars: bool, + pub get_uefi_var: Option<(String, String)>, + pub set_uefi_var: Option<(String, String, String)>, pub hash: Option, pub pd_addrs: Option<(u16, u16, u16)>, pub pd_ports: Option<(u8, u8, u8)>, @@ -304,6 +307,9 @@ pub fn parse(args: &[String]) -> Cli { // ec_hib_delay uptimeinfo: cli.uptimeinfo, s0ix_counter: cli.s0ix_counter, + // list_uefi_vars + // get_uefi_var + // set_uefi_var hash: cli.hash, pd_addrs: cli.pd_addrs, pd_ports: cli.pd_ports, @@ -1510,6 +1516,82 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } else { println!("s0ix_counter: Unknown"); } + } else if args.list_uefi_vars { + if let Some(vars) = os_specific::list_uefi_vars() { + for (name, guid) in &vars { + println!("{} {}", name, guid); + } + println!("\nTotal: {} variables", vars.len()); + } else { + println!("Failed to list UEFI variables"); + } + } else if let Some((name, guid)) = &args.get_uefi_var { + if let Some((data, attributes)) = os_specific::get_uefi_var(name, guid) { + println!("Variable: {}", name); + println!("GUID: {}", guid); + println!("Attrs: 0x{:08X}", attributes); + println!("Size: {} B", data.len()); + println!(); + // Print hex dump + for (i, chunk) in data.chunks(16).enumerate() { + print!("{:08X} ", i * 16); + for (j, byte) in chunk.iter().enumerate() { + print!("{:02X} ", byte); + if j == 7 { + print!(" "); + } + } + // Pad if last line is short + if chunk.len() < 16 { + for j in chunk.len()..16 { + print!(" "); + if j == 7 { + print!(" "); + } + } + } + print!(" |"); + for byte in chunk { + if byte.is_ascii_graphic() || *byte == b' ' { + print!("{}", *byte as char); + } else { + print!("."); + } + } + println!("|"); + } + } else { + println!("Failed to read UEFI variable '{}' ({})", name, guid); + } + } else if let Some((name, guid, filepath)) = &args.set_uefi_var { + #[cfg(feature = "uefi")] + let data = crate::uefi::fs::shell_read_file(filepath); + #[cfg(not(feature = "uefi"))] + let data = match fs::read(filepath) { + Ok(data) => Some(data), + Err(e) => { + println!("Error reading file: {:?}", e); + None + } + }; + if let Some(data) = data { + // Read existing attributes if the variable already exists, otherwise use sensible defaults + let attrs = if let Some((_, existing_attrs)) = os_specific::get_uefi_var(name, guid) { + existing_attrs + } else { + os_specific::EFI_VARIABLE_NON_VOLATILE + | os_specific::EFI_VARIABLE_BOOTSERVICE_ACCESS + | os_specific::EFI_VARIABLE_RUNTIME_ACCESS + }; + println!("Setting UEFI variable '{}' ({})", name, guid); + println!(" Attrs: 0x{:08X}", attrs); + println!(" Data size: {} B", data.len()); + if os_specific::set_uefi_var(name, guid, &data, attrs).is_some() { + println!(" Success"); + } else { + println!(" Failed to set UEFI variable"); + } + } } else if args.test { println!("Self-Test"); let result = selftest(&ec); diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index d08e6dd..c2c7995 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -85,6 +85,9 @@ pub fn parse(args: &[String]) -> Cli { ec_hib_delay: None, uptimeinfo: false, s0ix_counter: false, + list_uefi_vars: false, + get_uefi_var: None, + set_uefi_var: None, hash: None, // This is the only driver that works on UEFI driver: Some(CrosEcDriverType::Portio), @@ -671,6 +674,82 @@ pub fn parse(args: &[String]) -> Cli { None }; found_an_option = true; + } else if arg == "--list-uefi-vars" { + cli.list_uefi_vars = true; + found_an_option = true; + } else if arg == "--get-uefi-var" { + cli.get_uefi_var = if args.len() > i + 2 { + // Check if second arg looks like a GUID (contains '-') or is the next flag + if !args[i + 2].starts_with('-') && args[i + 2].contains('-') { + Some((args[i + 1].clone(), args[i + 2].clone())) + } else { + // Only name given, try lookup + let name = &args[i + 1]; + if let Some(guid) = crate::os_specific::known_uefi_guid(name) { + Some((name.clone(), guid.into())) + } else { + println!( + "Unknown UEFI variable '{}'. Please provide the GUID explicitly.", + name + ); + None + } + } + } else if args.len() > i + 1 { + let name = &args[i + 1]; + if let Some(guid) = crate::os_specific::known_uefi_guid(name) { + Some((name.clone(), guid.into())) + } else { + println!( + "Unknown UEFI variable '{}'. Please provide the GUID explicitly.", + name + ); + None + } + } else { + println!("--get-uefi-var requires at least one argument: NAME [GUID]"); + None + }; + found_an_option = true; + } else if arg == "--set-uefi-var" { + cli.set_uefi_var = if args.len() > i + 3 { + // Check if second arg looks like a GUID + if !args[i + 2].starts_with('-') && args[i + 2].contains('-') { + Some(( + args[i + 1].clone(), + args[i + 2].clone(), + args[i + 3].clone(), + )) + } else { + // Only name + filepath given, try lookup + let name = &args[i + 1]; + if let Some(guid) = crate::os_specific::known_uefi_guid(name) { + Some((name.clone(), guid.into(), args[i + 2].clone())) + } else { + println!( + "Unknown UEFI variable '{}'. Please provide the GUID explicitly.", + name + ); + None + } + } + } else if args.len() > i + 2 { + // Two args: name + filepath, try GUID lookup + let name = &args[i + 1]; + if let Some(guid) = crate::os_specific::known_uefi_guid(name) { + Some((name.clone(), guid.into(), args[i + 2].clone())) + } else { + println!( + "Unknown UEFI variable '{}'. Please provide the GUID explicitly.", + name + ); + None + } + } else { + println!("--set-uefi-var requires at least two arguments: NAME [GUID] FILEPATH"); + None + }; + found_an_option = true; } else if arg == "--pd-addrs" { cli.pd_addrs = if args.len() > i + 3 { let left = args[i + 1].parse::(); diff --git a/framework_lib/src/os_specific.rs b/framework_lib/src/os_specific.rs index 3566e4c..375b4b3 100644 --- a/framework_lib/src/os_specific.rs +++ b/framework_lib/src/os_specific.rs @@ -6,6 +6,8 @@ use std::thread; #[cfg(feature = "uefi")] use alloc::string::{String, ToString}; +#[cfg(feature = "uefi")] +use alloc::vec::Vec; // Could report the implemented UEFI spec version // But that's not very useful. Just look at the BIOS version @@ -37,6 +39,9 @@ pub fn get_os_version() -> String { } } +#[cfg(windows)] +use windows::{core::*, Win32::Foundation::*, Win32::System::WindowsProgramming::*}; + /// Sleep a number of microseconds pub fn sleep(micros: u64) { let duration = Duration::from_micros(micros); @@ -51,3 +56,205 @@ pub fn sleep(micros: u64) { uefi::boot::stall(duration); } } + +/// Look up the GUID for well-known UEFI variable names +pub fn known_uefi_guid(name: &str) -> Option<&'static str> { + match name { + // EFI_IMAGE_SECURITY_DATABASE_GUID + "db" | "dbx" | "dbt" | "dbr" => Some("d719b2cb-3d3a-4596-a3bc-dad00e67656f"), + // EFI_GLOBAL_VARIABLE + "PK" | "KEK" | "SecureBoot" | "SetupMode" | "BootOrder" | "BootCurrent" | "Timeout" => { + Some("8be4df61-93ca-11d2-aa0d-00e098032b8c") + } + // Insyde Security + "SecureFlashInfo" | "SecureFlashSetupMode" | "SecureFlashCertData" => { + Some("382af2bb-ffff-abcd-aaee-cce099338877") + } + _ => None, + } +} + +pub const EFI_VARIABLE_NON_VOLATILE: u32 = 0x00000001; +pub const EFI_VARIABLE_BOOTSERVICE_ACCESS: u32 = 0x00000002; +pub const EFI_VARIABLE_RUNTIME_ACCESS: u32 = 0x00000004; + +#[cfg(windows)] +pub fn get_uefi_var(name: &str, guid: &str) -> Option<(Vec, u32)> { + let mut data = [0u8; 65536]; + let mut attributes: u32 = 0; + let res = unsafe { + GetFirmwareEnvironmentVariableExW( + &HSTRING::from(name), + &HSTRING::from(format!("{{{guid}}}")), + Some(data.as_mut_ptr() as *mut core::ffi::c_void), + data.len() as u32, + Some(&mut attributes), + ) + }; + if res == 0 { + let error = unsafe { GetLastError() }; + error!("GetFirmwareEnvironmentVariableExW failed: {:?}", error); + return None; + } + Some((data[..res as usize].to_vec(), attributes)) +} + +#[cfg(windows)] +pub fn set_uefi_var(name: &str, guid: &str, value: &[u8], attributes: u32) -> Option<()> { + let res = unsafe { + SetFirmwareEnvironmentVariableExW( + &HSTRING::from(name), + &HSTRING::from(format!("{{{guid}}}")), + Some(value.as_ptr() as *const core::ffi::c_void), + value.len() as u32, + attributes, + ) + }; + if let Err(e) = res { + error!("SetFirmwareEnvironmentVariableExW failed: {:?}", e); + return None; + } + Some(()) +} + +#[cfg(target_os = "linux")] +pub fn get_uefi_var(name: &str, guid: &str) -> Option<(Vec, u32)> { + let path = format!("/sys/firmware/efi/efivars/{}-{}", name, guid); + let data = match std::fs::read(&path) { + Ok(data) => data, + Err(e) => { + error!("Failed to read efivar {}: {:?}", path, e); + return None; + } + }; + if data.len() < 4 { + error!("efivar file too short: {} bytes", data.len()); + return None; + } + let attributes = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); + Some((data[4..].to_vec(), attributes)) +} + +#[cfg(target_os = "linux")] +pub fn set_uefi_var(name: &str, guid: &str, value: &[u8], attributes: u32) -> Option<()> { + use std::io::Write; + let path = format!("/sys/firmware/efi/efivars/{}-{}", name, guid); + + // Clear the immutable flag if the file already exists + if std::path::Path::new(&path).exists() { + let file = match std::fs::File::open(&path) { + Ok(f) => f, + Err(e) => { + error!("Failed to open efivar {}: {:?}", path, e); + return None; + } + }; + use std::os::unix::io::AsRawFd; + let fd = file.as_raw_fd(); + let mut flags: libc::c_long = 0; + // FS_IOC_GETFLAGS + unsafe { + if libc::ioctl(fd, 0x80086601, &mut flags) != 0 { + error!("FS_IOC_GETFLAGS failed on {}", path); + return None; + } + } + // Clear FS_IMMUTABLE_FL (0x00000010) + flags &= !(0x00000010 as libc::c_long); + unsafe { + if libc::ioctl(fd, 0x40086602, &flags) != 0 { + error!("FS_IOC_SETFLAGS failed on {}", path); + return None; + } + } + } + + // Write attributes (4 bytes LE) + data + let mut file = match std::fs::File::create(&path) { + Ok(f) => f, + Err(e) => { + error!("Failed to create efivar {}: {:?}", path, e); + return None; + } + }; + let mut buf = Vec::with_capacity(4 + value.len()); + buf.extend_from_slice(&attributes.to_le_bytes()); + buf.extend_from_slice(value); + if let Err(e) = file.write_all(&buf) { + error!("Failed to write efivar {}: {:?}", path, e); + return None; + } + Some(()) +} + +#[cfg(target_os = "freebsd")] +pub fn get_uefi_var(_name: &str, _guid: &str) -> Option<(Vec, u32)> { + error!("Getting UEFI variable not yet supported on FreeBSD"); + None +} + +#[cfg(target_os = "freebsd")] +pub fn set_uefi_var(_name: &str, _guid: &str, _value: &[u8], _attributes: u32) -> Option<()> { + error!("Setting UEFI variable not yet supported on FreeBSD"); + None +} + +#[cfg(feature = "uefi")] +pub fn get_uefi_var(_name: &str, _guid: &str) -> Option<(Vec, u32)> { + error!("Getting UEFI variable not yet supported in UEFI shell"); + None +} + +#[cfg(feature = "uefi")] +pub fn set_uefi_var(_name: &str, _guid: &str, _value: &[u8], _attributes: u32) -> Option<()> { + error!("Setting UEFI variable not yet supported in UEFI shell"); + None +} + +#[cfg(target_os = "linux")] +pub fn list_uefi_vars() -> Option> { + let entries = match std::fs::read_dir("/sys/firmware/efi/efivars/") { + Ok(entries) => entries, + Err(e) => { + error!("Failed to read /sys/firmware/efi/efivars/: {:?}", e); + return None; + } + }; + let mut vars = Vec::new(); + for entry in entries { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let filename = entry.file_name().to_string_lossy().to_string(); + // Format: {name}-{8-4-4-4-12 guid} (guid is always 36 chars) + if filename.len() > 37 { + let split_pos = filename.len() - 37; + if filename.as_bytes()[split_pos] == b'-' { + let name = filename[..split_pos].to_string(); + let guid = filename[split_pos + 1..].to_string(); + vars.push((name, guid)); + } + } + } + vars.sort(); + Some(vars) +} + +#[cfg(windows)] +pub fn list_uefi_vars() -> Option> { + error!("Listing UEFI variables not yet supported on Windows"); + None +} + +#[cfg(target_os = "freebsd")] +pub fn list_uefi_vars() -> Option> { + error!("Listing UEFI variables not yet supported on FreeBSD"); + None +} + +#[cfg(feature = "uefi")] +pub fn list_uefi_vars() -> Option> { + error!("Listing UEFI variables not yet supported in UEFI shell"); + None +}