diff --git a/.changes/fix-binary-patching.md b/.changes/fix-binary-patching.md new file mode 100644 index 000000000000..0d23f230056c --- /dev/null +++ b/.changes/fix-binary-patching.md @@ -0,0 +1,8 @@ +--- +"tauri": patch:changes +"tauri-cli": patch:changes +"tauri-bundler": patch:changes +"@tauri-apps/cli": patch:changes +--- + +Change the way bundle type information is added to binary files. Intead of looking up value of a variable we simply look for default value. diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index 654ad5647401..f4916085fa27 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: MIT mod category; +#[cfg(any(target_os = "linux", target_os = "windows"))] +mod kmp; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] @@ -15,29 +17,46 @@ mod windows; use tauri_utils::{display_path, platform::Target as TargetPlatform}; +#[cfg(any(target_os = "linux", target_os = "windows"))] +const BUNDLE_VAR_TOKEN: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK"; /// Patch a binary with bundle type information +#[cfg(any(target_os = "linux", target_os = "windows"))] fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> { - match package_type { + let mut file_data = std::fs::read(binary).expect("Could not read binary file."); + + if let Some(bundle_var_index) = kmp::index_of(BUNDLE_VAR_TOKEN, &file_data) { #[cfg(target_os = "linux")] - PackageType::AppImage | PackageType::Deb | PackageType::Rpm => { - log::info!( - "Patching binary {:?} for type {}", - binary, - package_type.short_name() - ); - linux::patch_binary(binary, package_type)?; - } - PackageType::Nsis | PackageType::WindowsMsi => { - log::info!( - "Patching binary {:?} for type {}", - binary, - package_type.short_name() - ); - windows::patch_binary(binary, package_type)?; - } - _ => (), - } + let bundle_type = match package_type { + crate::PackageType::Deb => b"__TAURI_BUNDLE_TYPE_VAR_DEB", + crate::PackageType::Rpm => b"__TAURI_BUNDLE_TYPE_VAR_RPM", + crate::PackageType::AppImage => b"__TAURI_BUNDLE_TYPE_VAR_APP", + _ => { + return Err(crate::Error::InvalidPackageType( + package_type.short_name().to_owned(), + "Linux".to_owned(), + )) + } + }; + #[cfg(target_os = "windows")] + let bundle_type = match package_type { + crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", + crate::PackageType::WindowsMsi => b"__TAURI_BUNDLE_TYPE_VAR_MSI", + _ => { + return Err(crate::Error::InvalidPackageType( + package_type.short_name().to_owned(), + "Windows".to_owned(), + )) + } + }; + file_data[bundle_var_index..bundle_var_index + BUNDLE_VAR_TOKEN.len()] + .copy_from_slice(bundle_type); + + std::fs::write(binary, &file_data) + .map_err(|e| crate::Error::BinaryWriteError(e.to_string()))?; + } else { + return Err(crate::Error::MissingBundleTypeVar); + } Ok(()) } @@ -92,22 +111,17 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .expect("Main binary missing in settings"); let main_binary_path = settings.binary_path(main_binary); - // When packaging multiple binary types, we make a copy of the unsigned main_binary so that we can - // restore it after each package_type step. This avoids two issues: + // We make a copy of the unsigned main_binary so that we can restore it after each package_type step. + // This allows us to patch the binary correctly and avoids two issues: // - modifying a signed binary without updating its PE checksum can break signature verification // - codesigning tools should handle calculating+updating this, we just need to ensure // (re)signing is performed after every `patch_binary()` operation // - signing an already-signed binary can result in multiple signatures, causing verification errors - let main_binary_reset_required = matches!(target_os, TargetPlatform::Windows) - && settings.windows().can_sign() - && package_types.len() > 1; - let mut unsigned_main_binary_copy = tempfile::tempfile()?; - if main_binary_reset_required { - let mut unsigned_main_binary = std::fs::File::open(&main_binary_path)?; - std::io::copy(&mut unsigned_main_binary, &mut unsigned_main_binary_copy)?; - } + // TODO: change this to work on a copy while preserving the main binary unchanged + let mut main_binary_copy = tempfile::tempfile()?; + let mut main_binary_orignal = std::fs::File::open(&main_binary_path)?; + std::io::copy(&mut main_binary_orignal, &mut main_binary_copy)?; - let mut main_binary_signed = false; let mut bundles = Vec::::new(); for package_type in &package_types { // bundle was already built! e.g. DMG already built .app @@ -115,22 +129,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { continue; } + #[cfg(any(target_os = "linux", target_os = "windows"))] if let Err(e) = patch_binary(&main_binary_path, package_type) { log::warn!("Failed to add bundler type to the binary: {e}. Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues"); } // sign main binary for every package type after patch if matches!(target_os, TargetPlatform::Windows) && settings.windows().can_sign() { - if main_binary_signed && main_binary_reset_required { - let mut signed_main_binary = std::fs::OpenOptions::new() - .write(true) - .truncate(true) - .open(&main_binary_path)?; - unsigned_main_binary_copy.seek(SeekFrom::Start(0))?; - std::io::copy(&mut unsigned_main_binary_copy, &mut signed_main_binary)?; - } windows::sign::try_sign(&main_binary_path, settings)?; - main_binary_signed = true; } let bundle_paths = match package_type { @@ -172,6 +178,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { package_type: package_type.to_owned(), bundle_paths, }); + + // Restore unsigned and unpatched binary + let mut modified_main_binary = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(&main_binary_path)?; + main_binary_copy.seek(SeekFrom::Start(0))?; + std::io::copy(&mut main_binary_copy, &mut modified_main_binary)?; } if let Some(updater) = settings.updater() { diff --git a/crates/tauri-bundler/src/bundle/kmp/mod.rs b/crates/tauri-bundler/src/bundle/kmp/mod.rs new file mode 100644 index 000000000000..3e8489023044 --- /dev/null +++ b/crates/tauri-bundler/src/bundle/kmp/mod.rs @@ -0,0 +1,61 @@ +// Copyright 2016-2019 Cargo-Bundle developers +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Knuth–Morris–Pratt algorithm +// based on https://github.com/howeih/rust_kmp +#[cfg(any(target_os = "linux", target_os = "windows"))] +pub fn index_of(pattern: &[u8], target: &[u8]) -> Option { + let failure_function = find_failure_function(pattern); + + let mut t_i: usize = 0; + let mut p_i: usize = 0; + let target_len = target.len(); + let mut result_idx = None; + let pattern_len = pattern.len(); + + while (t_i < target_len) && (p_i < pattern_len) { + if target[t_i] == pattern[p_i] { + if result_idx.is_none() { + result_idx.replace(t_i); + } + t_i += 1; + p_i += 1; + if p_i >= pattern_len { + return result_idx; + } + } else { + if p_i == 0 { + p_i = 0; + t_i += 1; + } else { + p_i = failure_function[p_i - 1]; + } + result_idx = None; + } + } + None +} + +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn find_failure_function(pattern: &[u8]) -> Vec { + let mut i = 1; + let mut j = 0; + let pattern_length = pattern.len(); + let end_i = pattern_length - 1; + let mut failure_function = vec![0usize; pattern_length]; + while i <= end_i { + if pattern[i] == pattern[j] { + failure_function[i] = j + 1; + i += 1; + j += 1; + } else if j == 0 { + failure_function[i] = 0; + i += 1; + } else { + j = failure_function[j - 1]; + } + } + failure_function +} diff --git a/crates/tauri-bundler/src/bundle/linux/mod.rs b/crates/tauri-bundler/src/bundle/linux/mod.rs index ba66a8d397bb..8459b0528efb 100644 --- a/crates/tauri-bundler/src/bundle/linux/mod.rs +++ b/crates/tauri-bundler/src/bundle/linux/mod.rs @@ -7,8 +7,3 @@ pub mod appimage; pub mod debian; pub mod freedesktop; pub mod rpm; - -mod util; - -#[cfg(target_os = "linux")] -pub use util::patch_binary; diff --git a/crates/tauri-bundler/src/bundle/linux/util.rs b/crates/tauri-bundler/src/bundle/linux/util.rs deleted file mode 100644 index be125629581a..000000000000 --- a/crates/tauri-bundler/src/bundle/linux/util.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -/// Change value of __TAURI_BUNDLE_TYPE static variable to mark which package type it was bundled in -#[cfg(target_os = "linux")] -pub fn patch_binary( - binary_path: &std::path::PathBuf, - package_type: &crate::PackageType, -) -> crate::Result<()> { - let mut file_data = std::fs::read(binary_path).expect("Could not read binary file."); - - let elf = match goblin::Object::parse(&file_data)? { - goblin::Object::Elf(elf) => elf, - _ => return Err(crate::Error::GenericError("Not an ELF file".to_owned())), - }; - - let offset = find_bundle_type_symbol(elf).ok_or(crate::Error::MissingBundleTypeVar)?; - let offset = offset as usize; - if offset + 3 <= file_data.len() { - let chars = &mut file_data[offset..offset + 3]; - match package_type { - crate::PackageType::Deb => chars.copy_from_slice(b"DEB"), - crate::PackageType::Rpm => chars.copy_from_slice(b"RPM"), - crate::PackageType::AppImage => chars.copy_from_slice(b"APP"), - _ => { - return Err(crate::Error::InvalidPackageType( - package_type.short_name().to_owned(), - "linux".to_owned(), - )) - } - } - - std::fs::write(binary_path, &file_data) - .map_err(|error| crate::Error::BinaryWriteError(error.to_string()))?; - } else { - return Err(crate::Error::BinaryOffsetOutOfRange); - } - - Ok(()) -} - -/// Find address of a symbol in relocations table -#[cfg(target_os = "linux")] -fn find_bundle_type_symbol(elf: goblin::elf::Elf<'_>) -> Option { - for sym in elf.syms.iter() { - if let Some(name) = elf.strtab.get_at(sym.st_name) { - if name == "__TAURI_BUNDLE_TYPE" { - for reloc in elf.dynrelas.iter() { - if reloc.r_offset == sym.st_value { - return Some(reloc.r_addend.unwrap()); - } - } - } - } - } - - None -} diff --git a/crates/tauri-bundler/src/bundle/windows/mod.rs b/crates/tauri-bundler/src/bundle/windows/mod.rs index 366e000e1cca..b92fb5f56216 100644 --- a/crates/tauri-bundler/src/bundle/windows/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/mod.rs @@ -14,5 +14,3 @@ pub use util::{ NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME, }; - -pub use util::patch_binary; diff --git a/crates/tauri-bundler/src/bundle/windows/util.rs b/crates/tauri-bundler/src/bundle/windows/util.rs index 3685d0c06f9d..55cfea3a968f 100644 --- a/crates/tauri-bundler/src/bundle/windows/util.rs +++ b/crates/tauri-bundler/src/bundle/windows/util.rs @@ -77,75 +77,3 @@ pub fn os_bitness<'a>() -> Option<&'a str> { _ => None, } } - -pub fn patch_binary(binary_path: &PathBuf, package_type: &crate::PackageType) -> crate::Result<()> { - let mut file_data = std::fs::read(binary_path)?; - - let pe = match goblin::Object::parse(&file_data)? { - goblin::Object::PE(pe) => pe, - _ => { - return Err(crate::Error::BinaryParseError( - std::io::Error::new(std::io::ErrorKind::InvalidInput, "binary is not a PE file").into(), - )); - } - }; - - let tauri_bundle_section = pe - .sections - .iter() - .find(|s| s.name().unwrap_or_default() == ".taubndl") - .ok_or(crate::Error::MissingBundleTypeVar)?; - - let data_offset = tauri_bundle_section.pointer_to_raw_data as usize; - let pointer_size = if pe.is_64 { 8 } else { 4 }; - let ptr_bytes = file_data - .get(data_offset..data_offset + pointer_size) - .ok_or(crate::Error::BinaryOffsetOutOfRange)?; - // `try_into` is safe to `unwrap` here because we have already checked the slice's size through `get` - let ptr_value = if pe.is_64 { - u64::from_le_bytes(ptr_bytes.try_into().unwrap()) - } else { - u32::from_le_bytes(ptr_bytes.try_into().unwrap()).into() - }; - - let rdata_section = pe - .sections - .iter() - .find(|s| s.name().unwrap_or_default() == ".rdata") - .ok_or_else(|| { - crate::Error::BinaryParseError( - std::io::Error::new(std::io::ErrorKind::InvalidInput, ".rdata section not found").into(), - ) - })?; - - let rva = ptr_value.checked_sub(pe.image_base as u64).ok_or_else(|| { - crate::Error::BinaryParseError( - std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid RVA offset").into(), - ) - })?; - - // see "Relative virtual address (RVA)" for explanation of offset arithmetic here: - // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#general-concepts - let file_offset = rdata_section.pointer_to_raw_data as usize - + (rva as usize).saturating_sub(rdata_section.virtual_address as usize); - - // Overwrite the string at that offset - let string_bytes = file_data - .get_mut(file_offset..file_offset + 3) - .ok_or(crate::Error::BinaryOffsetOutOfRange)?; - match package_type { - crate::PackageType::Nsis => string_bytes.copy_from_slice(b"NSS"), - crate::PackageType::WindowsMsi => string_bytes.copy_from_slice(b"MSI"), - _ => { - return Err(crate::Error::InvalidPackageType( - package_type.short_name().to_owned(), - "windows".to_owned(), - )); - } - } - - std::fs::write(binary_path, &file_data) - .map_err(|e| crate::Error::BinaryWriteError(e.to_string()))?; - - Ok(()) -} diff --git a/crates/tauri-bundler/src/error.rs b/crates/tauri-bundler/src/error.rs index 518cb6ca9b71..40547a3eea39 100644 --- a/crates/tauri-bundler/src/error.rs +++ b/crates/tauri-bundler/src/error.rs @@ -99,19 +99,13 @@ pub enum Error { #[error("Wrong package type {0} for platform {1}")] InvalidPackageType(String, String), /// Bundle type symbol missing in binary - #[cfg_attr( - target_os = "linux", - error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date and that symbol stripping is disabled (https://doc.rust-lang.org/cargo/reference/profiles.html#strip)") - )] - #[cfg_attr( - not(target_os = "linux"), - error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date") - )] + #[error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date")] MissingBundleTypeVar, /// Failed to write binary file changed #[error("Failed to write binary file changes: `{0}`")] BinaryWriteError(String), /// Invalid offset while patching binary file + #[deprecated] #[error("Invalid offset while patching binary file")] BinaryOffsetOutOfRange, /// Unsupported architecture. diff --git a/crates/tauri-utils/src/platform.rs b/crates/tauri-utils/src/platform.rs index 542cea53c839..1b3f5a8f2957 100644 --- a/crates/tauri-utils/src/platform.rs +++ b/crates/tauri-utils/src/platform.rs @@ -353,18 +353,18 @@ fn resource_dir_from>( #[cfg_attr(target_vendor = "apple", link_section = "__DATA,taubndl")] // Marked as `mut` because it could get optimized away without it, // see https://github.com/tauri-apps/tauri/pull/13812 -static mut __TAURI_BUNDLE_TYPE: &str = "UNK"; +static mut __TAURI_BUNDLE_TYPE: &str = "__TAURI_BUNDLE_TYPE_VAR_UNK"; /// Get the type of the bundle current binary is packaged in. /// If the bundle type is unknown, it returns [`Option::None`]. pub fn bundle_type() -> Option { unsafe { match __TAURI_BUNDLE_TYPE { - "DEB" => Some(BundleType::Deb), - "RPM" => Some(BundleType::Rpm), - "APP" => Some(BundleType::AppImage), - "MSI" => Some(BundleType::Msi), - "NSS" => Some(BundleType::Nsis), + "__TAURI_BUNDLE_TYPE_VAR_DEB" => Some(BundleType::Deb), + "__TAURI_BUNDLE_TYPE_VAR_RPM" => Some(BundleType::Rpm), + "__TAURI_BUNDLE_TYPE_VAR_APP" => Some(BundleType::AppImage), + "__TAURI_BUNDLE_TYPE_VAR_MSI" => Some(BundleType::Msi), + "__TAURI_BUNDLE_TYPE_VAR_NSS" => Some(BundleType::Nsis), _ => { if cfg!(target_os = "macos") { Some(BundleType::App)