fix(bundler): patch bundle type via string replacement (#14521)

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: FabianLars <github@fabianlars.de>
This commit is contained in:
kandrelczyk
2026-01-18 23:51:02 +01:00
committed by GitHub
parent eccff97588
commit 0575dd287e
9 changed files with 131 additions and 195 deletions

View File

@@ -0,0 +1,8 @@
---
"tauri": minor:changes
"tauri-cli": minor:changes
"tauri-bundler": minor:changes
"@tauri-apps/cli": minor:changes
---
Change the way bundle type information is added to binary files. Instead of looking up the value of a variable we simply look for the default value.

View File

@@ -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 {
#[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 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")]
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<Vec<Bundle>> {
.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::<Bundle>::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<Vec<Bundle>> {
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<Vec<Bundle>> {
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() {

View File

@@ -0,0 +1,61 @@
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// KnuthMorrisPratt 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<usize> {
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<usize> {
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
}

View File

@@ -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;

View File

@@ -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<i64> {
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
}

View File

@@ -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;

View File

@@ -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(())
}

View File

@@ -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.

View File

@@ -344,23 +344,20 @@ fn resource_dir_from<P: AsRef<std::path::Path>>(
// Variable holding the type of bundle the executable is stored in. This is modified by binary
// patching during build
#[used]
#[no_mangle]
#[cfg_attr(not(target_vendor = "apple"), link_section = ".taubndl")]
#[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<BundleType> {
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)