From ae9304919b541000bdbf015d3ab6ec169123fe21 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sat, 11 Oct 2025 00:15:33 +0200 Subject: [PATCH 1/9] custom embed regions for lin, win, mac + macho code signature section removal for signing on macos --- crates/rustpatcher/Cargo.toml | 1 + crates/rustpatcher/README.md | 1 + crates/rustpatcher/examples/simple.rs | 6 +-- crates/rustpatcher/src/distributor.rs | 11 ++--- crates/rustpatcher/src/embed.rs | 61 ++++++++++++++++++--------- crates/rustpatcher/src/lib.rs | 13 +++--- crates/rustpatcher/src/macho.rs | 34 +++++++++++++++ crates/rustpatcher/src/patch.rs | 31 +++++++++----- crates/rustpatcher/src/patcher.rs | 17 ++++---- crates/rustpatcher/src/updater.rs | 17 +++++--- crates/rustpatcher/src/version.rs | 48 ++++++++++++++++++--- crates/rustpatcher/xtask/sign.rs | 44 ++++++++++++++++--- 12 files changed, 211 insertions(+), 73 deletions(-) create mode 100644 crates/rustpatcher/src/macho.rs diff --git a/crates/rustpatcher/Cargo.toml b/crates/rustpatcher/Cargo.toml index 03bf13f..dda3fe3 100644 --- a/crates/rustpatcher/Cargo.toml +++ b/crates/rustpatcher/Cargo.toml @@ -26,6 +26,7 @@ rand = "0.8" z32 = "1" clap = { version = "4", features = ["derive"] } once_cell = "1" +goblin = "0.10" tempfile = "3" nix = { version = "0.30", features = ["process"] } self-replace = "1" diff --git a/crates/rustpatcher/README.md b/crates/rustpatcher/README.md index 6ca6fac..d13b99d 100644 --- a/crates/rustpatcher/README.md +++ b/crates/rustpatcher/README.md @@ -13,6 +13,7 @@ Secure fully decentralized software updates. # Cargo.toml [dependencies] rustpatcher = "0.2" +rustpatcher-macros = "0.2" tokio = { version = "1", features = ["rt-multi-thread","macros"] } ``` diff --git a/crates/rustpatcher/examples/simple.rs b/crates/rustpatcher/examples/simple.rs index 0e39097..2a45f06 100644 --- a/crates/rustpatcher/examples/simple.rs +++ b/crates/rustpatcher/examples/simple.rs @@ -1,13 +1,11 @@ - #[tokio::main] #[rustpatcher::public_key("axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy")] async fn main() -> anyhow::Result<()> { - // Only in --release builds, not intended for debug builds rustpatcher::spawn(rustpatcher::UpdaterMode::At(13, 40)).await?; println!("my version is {:?}", rustpatcher::Version::current()?); - + #[cfg(not(debug_assertions))] println!("{:?}", rustpatcher::Patch::from_self()?.info()); #[cfg(debug_assertions)] @@ -22,4 +20,4 @@ async fn main() -> anyhow::Result<()> { } } Ok(()) -} \ No newline at end of file +} diff --git a/crates/rustpatcher/src/distributor.rs b/crates/rustpatcher/src/distributor.rs index 555b750..ee2ccfb 100644 --- a/crates/rustpatcher/src/distributor.rs +++ b/crates/rustpatcher/src/distributor.rs @@ -106,9 +106,7 @@ fn to_iroh_error(e: E) -> AcceptError where E: Into, { - AcceptError::User { - source: e.into(), - } + AcceptError::User { source: e.into() } } impl ProtocolHandler for Distributor { @@ -134,7 +132,7 @@ impl ProtocolHandler for Distributor { let mut accept_auth = false; for t in -1..2 { - if auth_buf == auth_hash(t, &owner_pub_key)[..] { + if auth_buf == auth_hash(t, owner_pub_key)[..] { accept_auth = true; break; } @@ -143,10 +141,7 @@ impl ProtocolHandler for Distributor { if !accept_auth { tx.write_u8(0).await.map_err(to_iroh_error)?; connection.close(VarInt::default(), b"auth failed"); - return Err(to_iroh_error(std::io::Error::new( - std::io::ErrorKind::Other, - "auth failed", - ))); + return Err(to_iroh_error(std::io::Error::other("auth failed"))); } else { tx.write_u8(1).await.map_err(to_iroh_error)?; } diff --git a/crates/rustpatcher/src/embed.rs b/crates/rustpatcher/src/embed.rs index 93f0f82..93e3efd 100644 --- a/crates/rustpatcher/src/embed.rs +++ b/crates/rustpatcher/src/embed.rs @@ -20,7 +20,9 @@ pub fn __set_owner_pub_key(pub_key: ed25519_dalek::VerifyingKey) { } pub fn get_owner_pub_key() -> &'static ed25519_dalek::VerifyingKey { - OWNER_PUB_KEY.get().expect("Owner public key not initialized") + OWNER_PUB_KEY + .get() + .expect("Owner public key not initialized") } pub fn get_app_version() -> &'static str { @@ -72,7 +74,10 @@ const _: () = { // Build const array without any runtime code or allocation #[doc(hidden)] -#[unsafe(link_section = ".embedded_signature")] +#[cfg_attr(target_os = "macos", unsafe(link_section = "__DATA,__embsig"))] +#[cfg_attr(target_os = "linux", unsafe(link_section = ".embsig"))] +#[cfg_attr(target_os = "windows", unsafe(link_section = ".embsig"))] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux", target_os = "windows")), unsafe(link_section = ".embsig"))] #[used] #[unsafe(no_mangle)] pub static EMBED_REGION: [u8; EMBED_REGION_LEN] = { @@ -149,10 +154,15 @@ pub static EMBED_REGION: [u8; EMBED_REGION_LEN] = { #[doc(hidden)] pub fn embed(version: &'static str, pub_key: &'static str) { __set_version(version); - __set_owner_pub_key(z32::decode(pub_key.as_bytes()).ok().and_then(|k_bytes| { - let key_array: [u8; 32] = k_bytes.try_into().ok()?; - ed25519_dalek::VerifyingKey::from_bytes(&key_array).ok() - }).expect("failed to decode public key")); + __set_owner_pub_key( + z32::decode(pub_key.as_bytes()) + .ok() + .and_then(|k_bytes| { + let key_array: [u8; 32] = k_bytes.try_into().ok()?; + ed25519_dalek::VerifyingKey::from_bytes(&key_array).ok() + }) + .expect("failed to decode public key"), + ); #[cfg(not(debug_assertions))] unsafe { core::ptr::read_volatile(&EMBED_REGION as *const _); @@ -166,7 +176,7 @@ pub struct EmbeddedRegion { } #[doc(hidden)] -pub fn cut_embed_section(bin_bytes: Vec) -> anyhow::Result<(Vec, Vec, EmbeddedRegion)> { +pub fn cut_embed_section(bin_bytes: &[u8]) -> anyhow::Result<(Vec, Vec, EmbeddedRegion)> { let start = bin_bytes .windows(EMBED_BOUNDS.len()) .position(|window| window == EMBED_BOUNDS) @@ -179,13 +189,13 @@ pub fn cut_embed_section(bin_bytes: Vec) -> anyhow::Result<(Vec, Vec if end as i128 - start as i128 != EMBED_REGION.len() as i128 { return Err(anyhow::anyhow!("invalid embed section size")); } - let mut out = bin_bytes; - let embed_region = out.drain(start..end).into_iter().collect::>(); + let mut out = bin_bytes.to_vec(); + let embed_region = out.drain(start..end).collect::>(); Ok((out, embed_region, EmbeddedRegion { start, end })) } #[doc(hidden)] -pub fn get_embedded_version(embed_region_bytes: &Vec) -> anyhow::Result { +pub fn get_embedded_version(embed_region_bytes: &[u8]) -> anyhow::Result { let version_offset = EMBED_BOUNDS.len() + BIN_HASH.len() + BIN_SIZE.len() + BIN_SIG.len(); let version_bytes = embed_region_bytes[version_offset..version_offset + VERSION_FIELD_LEN].to_vec(); @@ -194,8 +204,8 @@ pub fn get_embedded_version(embed_region_bytes: &Vec) -> anyhow::Result) -> anyhow::Result { - let (_, embed_region_bytes, _) = cut_embed_section(bin_data.clone())?; +pub fn get_embedded_patch_info(bin_data: &[u8]) -> anyhow::Result { + let (_, embed_region_bytes, _) = cut_embed_section(bin_data)?; let (_, buf) = embed_region_bytes.split_at(EMBED_BOUNDS.len()); let (hash_buf, buf) = buf.split_at(BIN_HASH.len()); @@ -203,9 +213,17 @@ pub fn get_embedded_patch_info(bin_data: &Vec) -> anyhow::Result) -> anyhow::Result, patch_info: PatchInfo,embed_region_bytes: EmbeddedRegion) -> anyhow::Result<()> { - +pub fn set_embedded_patch_info( + bin_data: &mut Vec, + patch_info: PatchInfo, + embed_region_bytes: EmbeddedRegion, +) -> anyhow::Result<()> { let (start, end) = (embed_region_bytes.start, embed_region_bytes.end); if end - start != EMBED_REGION_LEN { return Err(anyhow::anyhow!("invalid embed region length")); @@ -232,10 +253,12 @@ pub fn set_embedded_patch_info(bin_data: &mut Vec, patch_info: PatchInfo,emb region_buf.extend_from_slice(EMBED_BOUNDS); if region_buf.len() != EMBED_REGION_LEN { - return Err(anyhow::anyhow!("internal error: invalid embed region length")); + return Err(anyhow::anyhow!( + "internal error: invalid embed region length" + )); } bin_data.splice(start..end, region_buf.iter().cloned()); Ok(()) -} \ No newline at end of file +} diff --git a/crates/rustpatcher/src/lib.rs b/crates/rustpatcher/src/lib.rs index 46c9b97..c9a7713 100644 --- a/crates/rustpatcher/src/lib.rs +++ b/crates/rustpatcher/src/lib.rs @@ -1,9 +1,12 @@ -mod version; +mod distributor; mod patch; mod patcher; mod publisher; mod updater; -mod distributor; +mod version; + +#[cfg(target_os = "macos")] +pub mod macho; #[doc(hidden)] pub mod embed; @@ -12,14 +15,14 @@ pub mod embed; pub use version::Version; #[doc(hidden)] -pub use patch::{Patch,PatchInfo}; +pub use patch::{Patch, PatchInfo}; +use distributor::Distributor; use publisher::Publisher; use updater::Updater; -use distributor::Distributor; use embed::get_owner_pub_key; +pub use patcher::spawn; pub use rustpatcher_macros::*; pub use updater::UpdaterMode; -pub use patcher::spawn; diff --git a/crates/rustpatcher/src/macho.rs b/crates/rustpatcher/src/macho.rs new file mode 100644 index 0000000..68299f0 --- /dev/null +++ b/crates/rustpatcher/src/macho.rs @@ -0,0 +1,34 @@ +use anyhow::{anyhow, Result}; +use goblin::mach::{Mach, MachO}; + +pub fn exclude_code_signature(data: &[u8]) -> Result> { + let mach = Mach::parse(data)?; + + match mach { + Mach::Binary(macho) => exclude_from_macho(data, &macho), + Mach::Fat(_) => Err(anyhow!("Fat/Universal binaries not supported")), + } +} + +fn exclude_from_macho(data: &[u8], macho: &MachO) -> Result> { + for lc in &macho.load_commands { + if let goblin::mach::load_command::CommandVariant::CodeSignature(cmd) = lc.command { + let offset = cmd.dataoff as usize; + + if offset > data.len() { + return Err(anyhow!( + "Code signature offset out of bounds: offset={}, file_len={}", + offset, data.len() + )); + } + + // Cut the binary at the signature offset + // this makes the hash independent of signature size changes + // (resigning leeds to a new signature of a different size) + return Ok(data[..offset].to_vec()); + } + } + + // No code signature found, return original + Ok(data.to_vec()) +} \ No newline at end of file diff --git a/crates/rustpatcher/src/patch.rs b/crates/rustpatcher/src/patch.rs index 2333bbe..77aa7d3 100644 --- a/crates/rustpatcher/src/patch.rs +++ b/crates/rustpatcher/src/patch.rs @@ -1,7 +1,7 @@ +use crate::Version; use ed25519_dalek::{Signature, SigningKey, ed25519::signature::SignerMut}; use serde::{Deserialize, Serialize}; use sha2::Digest; -use crate::Version; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PatchInfo { @@ -27,7 +27,14 @@ impl Patch { } pub fn verify(&self) -> anyhow::Result<()> { - let (data_no_embed, _, _) = crate::embed::cut_embed_section(self.data.clone())?; + #[cfg(target_os = "macos")] + let data_stripped: Vec = crate::macho::exclude_code_signature(self.data.as_slice())?; + #[cfg(target_os = "macos")] + let data_stripped = data_stripped.as_slice(); + #[cfg(not(target_os = "macos"))] + let data_stripped = self.data.as_slice(); + + let (data_no_embed, _, _) = crate::embed::cut_embed_section(data_stripped)?; let mut data_hasher = sha2::Sha512::new(); data_hasher.update(&data_no_embed); @@ -52,8 +59,11 @@ impl Patch { Ok(()) } - pub fn sign(owner_signing_key: SigningKey, data_no_embed: Vec, version: Version, ) -> anyhow::Result { - + pub fn sign( + owner_signing_key: SigningKey, + data_no_embed: Vec, + version: Version, + ) -> anyhow::Result { let mut owner_siging_key = owner_signing_key; let mut data_hasher = sha2::Sha512::new(); data_hasher.update(data_no_embed.as_slice()); @@ -61,23 +71,22 @@ impl Patch { let mut sign_hash = sha2::Sha512::new(); sign_hash.update(version.to_string()); - sign_hash.update(&data_hash); + sign_hash.update(data_hash); sign_hash.update((data_no_embed.len() as u64).to_le_bytes()); let sign_hash = sign_hash.finalize(); let signature = owner_siging_key.sign(&sign_hash); Ok(PatchInfo { - version, - size: data_no_embed.len() as u64, - hash: data_hash, - signature, + version, + size: data_no_embed.len() as u64, + hash: data_hash, + signature, }) } pub fn from_self() -> anyhow::Result { - let data = std::fs::read(std::env::current_exe()?)?; - let patch_info = crate::embed::get_embedded_patch_info(&data)?; + let patch_info = crate::embed::get_embedded_patch_info(data.as_slice())?; let patch = Self { info: patch_info, diff --git a/crates/rustpatcher/src/patcher.rs b/crates/rustpatcher/src/patcher.rs index d40590c..9bede6e 100644 --- a/crates/rustpatcher/src/patcher.rs +++ b/crates/rustpatcher/src/patcher.rs @@ -1,8 +1,8 @@ use std::{str::FromStr, sync::Mutex}; use actor_helper::{Action, Actor, Handle}; -use distributed_topic_tracker::{RecordPublisher,RecordTopic}; -use iroh::{protocol::Router, Endpoint}; +use distributed_topic_tracker::{RecordPublisher, RecordTopic}; +use iroh::{Endpoint, protocol::Router}; use once_cell::sync::OnceCell; use sha2::Digest; use tracing::{error, warn}; @@ -12,7 +12,6 @@ use crate::{Distributor, Publisher, Updater, UpdaterMode}; static PATCHER: OnceCell>> = OnceCell::new(); pub async fn spawn(update_mode: UpdaterMode) -> anyhow::Result<()> { - #[cfg(not(debug_assertions))] if PATCHER.get().is_none() { let patcher = Patcher::builder().updater_mode(update_mode).build().await?; @@ -42,7 +41,6 @@ impl Default for Builder { } impl Builder { - #[cfg_attr(debug_assertions, allow(dead_code))] pub fn updater_mode(mut self, mode: UpdaterMode) -> Self { self.updater_mode = mode; @@ -52,10 +50,13 @@ impl Builder { #[cfg_attr(debug_assertions, allow(dead_code))] pub async fn build(self) -> anyhow::Result { let secret_key = iroh::SecretKey::generate(rand::rngs::OsRng); - let topic_id = RecordTopic::from_str(format!( - "rustpatcher:{}", - z32::encode(crate::embed::get_owner_pub_key().as_bytes()) - ).as_str())?; + let topic_id = RecordTopic::from_str( + format!( + "rustpatcher:{}", + z32::encode(crate::embed::get_owner_pub_key().as_bytes()) + ) + .as_str(), + )?; let mut hash = sha2::Sha512::new(); hash.update(topic_id.hash()); hash.update("v1"); diff --git a/crates/rustpatcher/src/updater.rs b/crates/rustpatcher/src/updater.rs index 1230818..e3b8632 100644 --- a/crates/rustpatcher/src/updater.rs +++ b/crates/rustpatcher/src/updater.rs @@ -1,5 +1,8 @@ use std::{ - env, ffi::{CString, OsString}, io::Write, process, ptr + env, + ffi::{CString, OsString}, + io::Write, + process, ptr, }; use actor_helper::{Action, Actor, Handle}; @@ -32,7 +35,7 @@ struct UpdaterActor { newer_patch: Option, record_publisher: RecordPublisher, try_update_interval: tokio::time::Interval, - + self_path_before_replace: Option, } @@ -108,7 +111,7 @@ impl UpdaterActor { async fn check_for_updates(&mut self) -> anyhow::Result> { let now = unix_minute(0); let mut records = self.record_publisher.get_records(now).await; - records.extend(self.record_publisher.get_records(now-1).await); + records.extend(self.record_publisher.get_records(now - 1).await); let c_version = Version::current()?; let mut newer_patch_infos = records .iter() @@ -151,11 +154,10 @@ impl UpdaterActor { let patch = res?; self.newer_patch = Some(patch.clone()); - self.self_path_before_replace = Some(env::current_exe()?.into()); let mut temp_file = tempfile::NamedTempFile::new()?; - temp_file.write_all(&patch.data())?; + temp_file.write_all(patch.data())?; let path = temp_file.path(); self_replace::self_replace(path)?; @@ -168,7 +170,10 @@ impl UpdaterActor { } async fn restart_after_update(&mut self) -> anyhow::Result<()> { - let exe_raw = self.self_path_before_replace.clone().ok_or(anyhow::anyhow!("no self path stored"))?; + let exe_raw = self + .self_path_before_replace + .clone() + .ok_or(anyhow::anyhow!("no self path stored"))?; let exe = CString::new(exe_raw.to_str().unwrap())?; // The array must be null-terminated. diff --git a/crates/rustpatcher/src/version.rs b/crates/rustpatcher/src/version.rs index 6d2bb66..d7c3f3a 100644 --- a/crates/rustpatcher/src/version.rs +++ b/crates/rustpatcher/src/version.rs @@ -1,15 +1,15 @@ -use std::str::FromStr; +use std::{fmt::Display, str::FromStr}; use anyhow::bail; use serde::{Deserialize, Serialize}; use tracing::warn; -#[derive(Debug, Clone, Serialize, Deserialize,PartialOrd, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Version(pub i32, pub i32, pub i32); -impl ToString for Version { - fn to_string(&self) -> String { - format!("{}.{}.{}",self.0,self.1,self.2) +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.0, self.1, self.2) } } @@ -31,6 +31,12 @@ impl FromStr for Version { } } +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Ord for Version { fn cmp(&self, other: &Self) -> std::cmp::Ordering { let self_sum: i128 = ((i32::MAX as i128).pow(2) * self.0 as i128) @@ -41,6 +47,34 @@ impl Ord for Version { + other.2 as i128; self_sum.cmp(&other_sum) } + + fn max(self, other: Self) -> Self + where + Self: Sized, + { + if other > self { other } else { self } + } + + fn min(self, other: Self) -> Self + where + Self: Sized, + { + if other < self { other } else { self } + } + + fn clamp(self, min: Self, max: Self) -> Self + where + Self: Sized, + { + assert!(min <= max); + if self < min { + min + } else if self > max { + max + } else { + self + } + } } impl Version { @@ -49,8 +83,8 @@ impl Version { } pub fn current() -> anyhow::Result { - let v = Version::from_str(&crate::embed::get_app_version()); + let v = Version::from_str(crate::embed::get_app_version()); warn!("Current version: {:?}", v); v } -} \ No newline at end of file +} diff --git a/crates/rustpatcher/xtask/sign.rs b/crates/rustpatcher/xtask/sign.rs index 89befba..c12765e 100644 --- a/crates/rustpatcher/xtask/sign.rs +++ b/crates/rustpatcher/xtask/sign.rs @@ -1,4 +1,8 @@ -use std::{fs::{self, OpenOptions}, io::{Seek, SeekFrom, Write}, path::PathBuf}; +use std::{ + fs::{self, OpenOptions}, + io::{Seek, SeekFrom, Write}, + path::PathBuf, +}; use clap::{Parser, Subcommand}; use ed25519_dalek::SigningKey; @@ -40,7 +44,7 @@ fn main() -> anyhow::Result<()> { match root.cmd { Commands::Sign(args) => sign_cmd(args), Commands::Gen { key_file } => generate_key_cmd(key_file), - } + } } fn generate_key_cmd(key_file: std::path::PathBuf) -> anyhow::Result<()> { @@ -55,10 +59,16 @@ fn generate_key_cmd(key_file: std::path::PathBuf) -> anyhow::Result<()> { fs::write(&key_file, signing_key_bytes)?; println!("Wrote signing key to {}", key_file.display()); - println!("Public key (z-base-32): {}", z32::encode(signing_key.verifying_key().as_bytes())); + println!( + "Public key (z-base-32): {}", + z32::encode(signing_key.verifying_key().as_bytes()) + ); println!("\n"); println!("// Add the following to your main function:\n"); - println!("[rustpatcher::public_key(\"{}\")]",z32::encode(signing_key.verifying_key().as_bytes())); + println!( + "#[rustpatcher::public_key(\"{}\")]", + z32::encode(signing_key.verifying_key().as_bytes()) + ); println!("fn main() {{\n // your code here\n}}"); Ok(()) @@ -84,9 +94,16 @@ fn sign_cmd(args: SignArgs) -> anyhow::Result<()> { let mut data = fs::read(&args.binary) .map_err(|e| anyhow::anyhow!("failed to read binary {}: {}", args.binary.display(), e))?; + + #[cfg(target_os = "macos")] + let data_stripped: Vec = rustpatcher::macho::exclude_code_signature(data.as_slice())?; + #[cfg(target_os = "macos")] + let data_stripped = data_stripped.as_slice(); + #[cfg(not(target_os = "macos"))] + let data_stripped = self.data.as_slice(); let (data_no_embed, data_embed, embed_region) = - rustpatcher::embed::cut_embed_section(data.clone())?; + rustpatcher::embed::cut_embed_section(data_stripped)?; let version = rustpatcher::embed::get_embedded_version(&data_embed)?; let patch_info = rustpatcher::Patch::sign(signing_key, data_no_embed, version)?; @@ -95,6 +112,23 @@ fn sign_cmd(args: SignArgs) -> anyhow::Result<()> { file.seek(SeekFrom::Start(0))?; file.write_all(&data)?; file.set_len(data.len() as u64)?; + drop(file); + + #[cfg(target_os = "macos")] + { + // re-sign the binary with codesign + let status = std::process::Command::new("codesign") + .arg("--force") + .arg("--sign") + .arg("-") + .arg(args.binary) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status()?; + if !status.success() { + return Err(anyhow::anyhow!("mac os specific codesign failed")); + } + } Ok(()) } From a0baa120119df9406169855481b023deb2986dcf Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 00:01:10 +0200 Subject: [PATCH 2/9] fixed double signature bug + fixed updater process exit if no logging enabled bug --- crates/rustpatcher/Cargo.toml | 2 +- crates/rustpatcher/examples/platforms.rs | 28 +++++------ crates/rustpatcher/examples/simple.rs | 18 +++---- crates/rustpatcher/src/patch.rs | 15 +++++- crates/rustpatcher/src/updater.rs | 9 +++- crates/rustpatcher/xtask/sign.rs | 60 +++++++++++++----------- 6 files changed, 74 insertions(+), 58 deletions(-) diff --git a/crates/rustpatcher/Cargo.toml b/crates/rustpatcher/Cargo.toml index dda3fe3..2919c04 100644 --- a/crates/rustpatcher/Cargo.toml +++ b/crates/rustpatcher/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpatcher" -version = "0.2.0" +version = "0.2.1" edition = "2024" description = "distributed patching system for single binary applications" license = "MIT" diff --git a/crates/rustpatcher/examples/platforms.rs b/crates/rustpatcher/examples/platforms.rs index ea2dff1..19bd228 100644 --- a/crates/rustpatcher/examples/platforms.rs +++ b/crates/rustpatcher/examples/platforms.rs @@ -1,9 +1,9 @@ #[cfg(target_os = "windows")] -const PUBLIC_KEY: &'static str = "...windows-key..."; +const PUBLIC_KEY: &str = "...windows-key..."; #[cfg(target_os = "linux")] -const PUBLIC_KEY: &'static str = "...linux-key..."; +const PUBLIC_KEY: &str = "...linux-key..."; #[cfg(target_os = "macos")] -const PUBLIC_KEY: &'static str = "...macos-key..."; +const PUBLIC_KEY: &str = "...macos-key..."; #[rustpatcher::public_key(PUBLIC_KEY)] #[tokio::main] @@ -13,18 +13,16 @@ async fn main() -> anyhow::Result<()> { .with_thread_ids(true) .init(); - rustpatcher::spawn(rustpatcher::UpdaterMode::At(13, 40)).await?; + + #[cfg(not(debug_assertions))] + { + rustpatcher::spawn(rustpatcher::UpdaterMode::At(13, 40)).await?; - let self_patch = rustpatcher::Patch::from_self()?; - println!("my version {:?} running", self_patch.info().version); - - loop { - tokio::select! { - _ = tokio::signal::ctrl_c() => { - println!("Exiting on Ctrl-C"); - break; - } - } + let self_patch = rustpatcher::Patch::from_self()?; + println!("my version {:?} running", self_patch.info().version); } - Ok(()) + + tokio::signal::ctrl_c() + .await + .map_err(|e| anyhow::anyhow!(e)) } diff --git a/crates/rustpatcher/examples/simple.rs b/crates/rustpatcher/examples/simple.rs index 2a45f06..52b802f 100644 --- a/crates/rustpatcher/examples/simple.rs +++ b/crates/rustpatcher/examples/simple.rs @@ -1,8 +1,10 @@ +const PUBLIC_KEY: &str = "axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy"; + #[tokio::main] -#[rustpatcher::public_key("axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy")] +#[rustpatcher::public_key(PUBLIC_KEY)] async fn main() -> anyhow::Result<()> { // Only in --release builds, not intended for debug builds - rustpatcher::spawn(rustpatcher::UpdaterMode::At(13, 40)).await?; + rustpatcher::spawn(rustpatcher::UpdaterMode::Now).await?; println!("my version is {:?}", rustpatcher::Version::current()?); @@ -11,13 +13,7 @@ async fn main() -> anyhow::Result<()> { #[cfg(debug_assertions)] println!("Debug build, skipping Patch::from_self()"); - loop { - tokio::select! { - _ = tokio::signal::ctrl_c() => { - println!("Exiting on Ctrl-C"); - break; - } - } - } - Ok(()) + tokio::signal::ctrl_c() + .await + .map_err(|e| anyhow::anyhow!(e)) } diff --git a/crates/rustpatcher/src/patch.rs b/crates/rustpatcher/src/patch.rs index 77aa7d3..7bbb9ef 100644 --- a/crates/rustpatcher/src/patch.rs +++ b/crates/rustpatcher/src/patch.rs @@ -61,9 +61,20 @@ impl Patch { pub fn sign( owner_signing_key: SigningKey, - data_no_embed: Vec, - version: Version, + data: &[u8], ) -> anyhow::Result { + + #[cfg(target_os = "macos")] + let data_stripped = crate::macho::exclude_code_signature(data)?; + #[cfg(target_os = "macos")] + let data_stripped = data_stripped.as_slice(); + #[cfg(not(target_os = "macos"))] + let data_stripped = self.data.as_slice(); + + let (data_no_embed, data_embed, _) = + crate::embed::cut_embed_section(data_stripped)?; + let version = crate::embed::get_embedded_version(&data_embed)?; + let mut owner_siging_key = owner_signing_key; let mut data_hasher = sha2::Sha512::new(); data_hasher.update(data_no_embed.as_slice()); diff --git a/crates/rustpatcher/src/updater.rs b/crates/rustpatcher/src/updater.rs index e3b8632..2d37aa4 100644 --- a/crates/rustpatcher/src/updater.rs +++ b/crates/rustpatcher/src/updater.rs @@ -180,8 +180,13 @@ impl UpdaterActor { let args: [*const libc::c_char; 1] = [ptr::null()]; unsafe { - info!("execv: {:?}", nix::libc::execv(exe.as_ptr(), args.as_ptr())); + let res = nix::libc::execv(exe.as_ptr(), args.as_ptr()); + if res != 0 { + let err = std::io::Error::last_os_error(); + error!("execv failed: {:?}", err); + return Err(anyhow::anyhow!("execv failed: {:?}", err)); + } + process::exit(0); } - process::exit(0); } } diff --git a/crates/rustpatcher/xtask/sign.rs b/crates/rustpatcher/xtask/sign.rs index c12765e..e93bd82 100644 --- a/crates/rustpatcher/xtask/sign.rs +++ b/crates/rustpatcher/xtask/sign.rs @@ -86,6 +86,16 @@ fn sign_cmd(args: SignArgs) -> anyhow::Result<()> { let signing_key = load_signing_key(key_src)?; + // sign with codesign tool to keep the signature length etc consistent. + // the code signature might change in length from ld64 and codesign (at least thats my theory right now) + // if we first refresh the signature with the codesign tool, then compute our signautre (-codesign block at end data[..offset]) + // this still leaves us with the signature dependent LC_CODE_SIGNATURE header but it only holds offset and size + // wich are constant when using the identical signature method (codesign instead of ld64 in this case). + // we compute our signature over the data without the codesign block (data[..offset]) + // then we sign again with codesign after embedding our patch info. + #[cfg(target_os = "macos")] + macos_codesign(&args.binary)?; + let mut file = OpenOptions::new() .read(true) .write(true) @@ -93,20 +103,10 @@ fn sign_cmd(args: SignArgs) -> anyhow::Result<()> { .open(&args.binary)?; let mut data = fs::read(&args.binary) - .map_err(|e| anyhow::anyhow!("failed to read binary {}: {}", args.binary.display(), e))?; - - #[cfg(target_os = "macos")] - let data_stripped: Vec = rustpatcher::macho::exclude_code_signature(data.as_slice())?; - #[cfg(target_os = "macos")] - let data_stripped = data_stripped.as_slice(); - #[cfg(not(target_os = "macos"))] - let data_stripped = self.data.as_slice(); - - let (data_no_embed, data_embed, embed_region) = - rustpatcher::embed::cut_embed_section(data_stripped)?; - let version = rustpatcher::embed::get_embedded_version(&data_embed)?; + .map_err(|e| anyhow::anyhow!("failed to read binary {}: {}", args.binary.display(), e))?; - let patch_info = rustpatcher::Patch::sign(signing_key, data_no_embed, version)?; + let patch_info = rustpatcher::Patch::sign(signing_key, data.as_slice())?; + let (_,_,embed_region) = rustpatcher::embed::cut_embed_section(data.as_slice())?; rustpatcher::embed::set_embedded_patch_info(&mut data, patch_info, embed_region)?; file.seek(SeekFrom::Start(0))?; @@ -114,22 +114,28 @@ fn sign_cmd(args: SignArgs) -> anyhow::Result<()> { file.set_len(data.len() as u64)?; drop(file); + // post sign (see comment above) #[cfg(target_os = "macos")] - { - // re-sign the binary with codesign - let status = std::process::Command::new("codesign") - .arg("--force") - .arg("--sign") - .arg("-") - .arg(args.binary) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status()?; - if !status.success() { - return Err(anyhow::anyhow!("mac os specific codesign failed")); - } - } + macos_codesign(&args.binary)?; + + Ok(()) +} +#[cfg(target_os = "macos")] +fn macos_codesign(binary: &PathBuf) -> anyhow::Result<()> { + + // re-sign the binary with codesign + let status = std::process::Command::new("codesign") + .arg("--force") + .arg("--sign") + .arg("-") + .arg(binary) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status()?; + if !status.success() { + return Err(anyhow::anyhow!("mac os specific codesign failed")); + } Ok(()) } From a0d2e89eda89b71faf7f58803a0876d75d16f995 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 00:39:56 +0200 Subject: [PATCH 3/9] debug: e2e test github actions --- .github/workflows/e2e-test.yml | 281 +++++++++++++++++++++++++++++ crates/rustpatcher/Cargo.toml | 6 +- crates/rustpatcher/e2e_test/e2e.rs | 18 ++ 3 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/e2e-test.yml create mode 100644 crates/rustpatcher/e2e_test/e2e.rs diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000..33ea106 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,281 @@ +name: E2E Cross-Platform Update Test + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + build-and-test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + platform: linux + binary_ext: "" + key_file: owner_key_linux + timeout_cmd: timeout + - os: macos-latest + platform: macos + binary_ext: "" + key_file: owner_key_macos + timeout_cmd: gtimeout + - os: windows-latest + platform: win + binary_ext: ".exe" + key_file: owner_key_win + timeout_cmd: timeout + + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + defaults: + run: + shell: bash + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + # macOS specific: Install coreutils for gtimeout and verify codesign + - name: Install dependencies (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install coreutils + if ! command -v codesign &> /dev/null; then + echo "codesign command not found!" + exit 1 + fi + echo "codesign is available" + codesign --version || true + + - name: Build e2e example (initial version) + working-directory: crates/rustpatcher + run: cargo build --release --example e2e + + - name: Sign initial binary + working-directory: crates/rustpatcher + run: | + cargo run --release --bin rustpatcher sign \ + ../../target/release/examples/e2e${{ matrix.binary_ext }} \ + --key-file e2e_test/keys/${{ matrix.key_file }} + + - name: Save initial binary + run: | + mkdir -p old_version + cp target/release/examples/e2e${{ matrix.binary_ext }} old_version/e2e${{ matrix.binary_ext }} + shell: bash + + - name: Read and bump version + id: version + working-directory: crates/rustpatcher + run: | + # Extract current version from Cargo.toml + CURRENT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + echo "Current version: $CURRENT_VERSION" + + # Parse version components + MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1) + MINOR=$(echo $CURRENT_VERSION | cut -d. -f2) + PATCH=$(echo $CURRENT_VERSION | cut -d. -f3) + + # Bump patch version + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + + echo "New version: $NEW_VERSION" + echo "old_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + shell: bash + + - name: Update Cargo.toml version + working-directory: crates/rustpatcher + run: | + # Update version in Cargo.toml + if [ "${{ matrix.os }}" = "windows-latest" ]; then + sed -i 's/version = "${{ steps.version.outputs.old_version }}"/version = "${{ steps.version.outputs.new_version }}"/' Cargo.toml + else + sed -i.bak 's/version = "${{ steps.version.outputs.old_version }}"/version = "${{ steps.version.outputs.new_version }}"/' Cargo.toml + rm -f Cargo.toml.bak + fi + echo "Updated version from ${{ steps.version.outputs.old_version }} to ${{ steps.version.outputs.new_version }}" + grep '^version = ' Cargo.toml + shell: bash + + - name: Build e2e example (new version) + working-directory: crates/rustpatcher + run: cargo build --release --example e2e + + - name: Sign new binary + working-directory: crates/rustpatcher + run: | + cargo run --release --bin rustpatcher sign \ + ../../target/release/examples/e2e${{ matrix.binary_ext }} \ + --key-file e2e_test/keys/${{ matrix.key_file }} + + - name: Save new binary + run: | + mkdir -p new_version + cp target/release/examples/e2e${{ matrix.binary_ext }} new_version/e2e${{ matrix.binary_ext }} + shell: bash + + - name: Make binaries executable + if: matrix.os != 'windows-latest' + run: | + chmod +x old_version/e2e + chmod +x new_version/e2e + + - name: Run E2E test + timeout-minutes: 5 + env: + OLD_VERSION: ${{ steps.version.outputs.old_version }} + NEW_VERSION: ${{ steps.version.outputs.new_version }} + run: | + echo "=== Starting E2E Update Test for ${{ matrix.platform }} ===" + echo "Testing P2P update from $OLD_VERSION to $NEW_VERSION" + + # Parse version patterns + OLD_MAJOR=$(echo $OLD_VERSION | cut -d. -f1) + OLD_MINOR=$(echo $OLD_VERSION | cut -d. -f2) + OLD_PATCH=$(echo $OLD_VERSION | cut -d. -f3) + NEW_MAJOR=$(echo $NEW_VERSION | cut -d. -f1) + NEW_MINOR=$(echo $NEW_VERSION | cut -d. -f2) + NEW_PATCH=$(echo $NEW_VERSION | cut -d. -f3) + + OLD_PATTERN="Version($OLD_MAJOR, $OLD_MINOR, $OLD_PATCH)" + NEW_PATTERN="Version($NEW_MAJOR, $NEW_MINOR, $NEW_PATCH)" + + echo "Expected initial version: $OLD_PATTERN" + echo "Expected after update: $NEW_PATTERN" + echo "" + + # Start the NEW version (publisher/server with the update) + echo "--- Starting NEW version binary (publisher) ---" + ./new_version/e2e${{ matrix.binary_ext }} > new_output.txt 2>&1 & + NEW_PID=$! + echo "Started new version with PID: $NEW_PID" + sleep 2 + + # Start the OLD version (client that should update itself) + echo "--- Starting OLD version binary (to be updated) ---" + ./old_version/e2e${{ matrix.binary_ext }} > old_output.txt 2>&1 & + OLD_PID=$! + echo "Started old version with PID: $OLD_PID" + + # Monitor old version output for up to 120 seconds (2 minutes) + echo "" + echo "Monitoring for update... (timeout: 120 seconds)" + TIMEOUT=120 + ELAPSED=0 + UPDATE_SUCCESS=false + + while [ $ELAPSED -lt $TIMEOUT ]; do + if [ -f old_output.txt ]; then + # Check if we see the NEW version pattern (update succeeded) + if grep -q "$NEW_PATTERN" old_output.txt; then + echo "UPDATE SUCCESSFUL! Old version updated to $NEW_PATTERN" + UPDATE_SUCCESS=true + break + fi + fi + sleep 5 + ELAPSED=$((ELAPSED + 5)) + echo " ... $ELAPSED seconds elapsed" + done + + # Kill both processes + kill $NEW_PID $OLD_PID 2>/dev/null || true + sleep 1 + + # Show outputs + echo "" + echo "=== New version output ===" + cat new_output.txt || echo "No output" + echo "" + echo "=== Old version output ===" + cat old_output.txt || echo "No output" + echo "" + + # Evaluate result + if [ "$UPDATE_SUCCESS" = true ]; then + echo "E2E test PASSED for ${{ matrix.platform }}!" + echo "The old version successfully updated itself via P2P network" + exit 0 + else + echo "E2E test FAILED for ${{ matrix.platform }}" + echo "The old version did NOT update to $NEW_PATTERN within 2 minutes" + echo "" + echo "Possible issues:" + echo " - P2P discovery failed" + echo " - Network connectivity issues" + echo " - Update mechanism not triggering" + echo " - Check outputs above for errors" + exit 1 + fi + + # Upload artifacts for debugging + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-artifacts-${{ matrix.platform }} + path: | + old_version/ + new_version/ + old_output.txt + new_output.txt + old_error.txt + new_error.txt + retention-days: 7 + + - name: Test Summary + if: success() + run: | + echo "E2E update test completed successfully for ${{ matrix.platform }}" + echo "- Built and signed version ${{ steps.version.outputs.old_version }}" + echo "- Built and signed version ${{ steps.version.outputs.new_version }}" + echo "- Old version successfully updated itself via P2P to new version" + echo "- Update mechanism working correctly!" + shell: bash + + summary: + needs: build-and-test + runs-on: ubuntu-latest + if: always() + steps: + - name: Check test results + run: | + if [ "${{ needs.build-and-test.result }}" = "success" ]; then + echo "All E2E tests passed across all platforms!" + else + echo "Some E2E tests failed. Check the logs above." + exit 1 + fi diff --git a/crates/rustpatcher/Cargo.toml b/crates/rustpatcher/Cargo.toml index 2919c04..44dd8fe 100644 --- a/crates/rustpatcher/Cargo.toml +++ b/crates/rustpatcher/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpatcher" -version = "0.2.1" +version = "0.2.2" edition = "2024" description = "distributed patching system for single binary applications" license = "MIT" @@ -47,6 +47,10 @@ path = "examples/platforms.rs" name = "simple" path = "examples/simple.rs" +[[example]] +name = "e2e" +path = "e2e_test/e2e.rs" + [[bin]] name = "rustpatcher" path = "xtask/sign.rs" diff --git a/crates/rustpatcher/e2e_test/e2e.rs b/crates/rustpatcher/e2e_test/e2e.rs new file mode 100644 index 0000000..91c2eab --- /dev/null +++ b/crates/rustpatcher/e2e_test/e2e.rs @@ -0,0 +1,18 @@ +#[cfg(target_os = "macos")] +const PUBLIC_KEY: &str = "9mrnh6bhosexei8ciwe1gm7kqitg7y3rbzjbezqbncpg1sk6sq6o"; +#[cfg(target_os = "linux")] +const PUBLIC_KEY: &str = "6qdxs69eg39f1iu79sza56tqbzzgur4gteowp9fa8dwfpakc3ngy"; +#[cfg(target_os = "windows")] +const PUBLIC_KEY: &str = "bhafqhm8k9e7fzab7i7h6gie6oedncwyffautkngqsa9d1ohzuho"; + +#[tokio::main] +#[rustpatcher::public_key(PUBLIC_KEY)] +async fn main() -> anyhow::Result<()> { + + rustpatcher::spawn(rustpatcher::UpdaterMode::Now).await?; + println!("{:?}", rustpatcher::Version::current()?); + + tokio::signal::ctrl_c() + .await + .map_err(|e| anyhow::anyhow!(e)) +} From e5531ffa40fb678a62505a77f749c58253f58662 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 00:45:39 +0200 Subject: [PATCH 4/9] fixed self.data -> data in patch.rs --- crates/rustpatcher/src/patch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rustpatcher/src/patch.rs b/crates/rustpatcher/src/patch.rs index 7bbb9ef..c36162e 100644 --- a/crates/rustpatcher/src/patch.rs +++ b/crates/rustpatcher/src/patch.rs @@ -69,7 +69,7 @@ impl Patch { #[cfg(target_os = "macos")] let data_stripped = data_stripped.as_slice(); #[cfg(not(target_os = "macos"))] - let data_stripped = self.data.as_slice(); + let data_stripped = data; let (data_no_embed, data_embed, _) = crate::embed::cut_embed_section(data_stripped)?; From a5b2f7ab05ed43a1e242d3e456e446f75161e653 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 00:47:47 +0200 Subject: [PATCH 5/9] added missing signing keys for e2e test --- crates/rustpatcher/e2e_test/keys/owner_key_linux | 1 + crates/rustpatcher/e2e_test/keys/owner_key_macos | 1 + crates/rustpatcher/e2e_test/keys/owner_key_win | 1 + 3 files changed, 3 insertions(+) create mode 100644 crates/rustpatcher/e2e_test/keys/owner_key_linux create mode 100644 crates/rustpatcher/e2e_test/keys/owner_key_macos create mode 100644 crates/rustpatcher/e2e_test/keys/owner_key_win diff --git a/crates/rustpatcher/e2e_test/keys/owner_key_linux b/crates/rustpatcher/e2e_test/keys/owner_key_linux new file mode 100644 index 0000000..e057991 --- /dev/null +++ b/crates/rustpatcher/e2e_test/keys/owner_key_linux @@ -0,0 +1 @@ +97zzdmnb7p6mt8zsqc74wtqqqzaw39on4jsoiriozdiq3kquteso \ No newline at end of file diff --git a/crates/rustpatcher/e2e_test/keys/owner_key_macos b/crates/rustpatcher/e2e_test/keys/owner_key_macos new file mode 100644 index 0000000..81ceeb6 --- /dev/null +++ b/crates/rustpatcher/e2e_test/keys/owner_key_macos @@ -0,0 +1 @@ +7xqhfrze8we3dcuywwm74ctbbcnefyer4u6iph4nccontc7k67yy \ No newline at end of file diff --git a/crates/rustpatcher/e2e_test/keys/owner_key_win b/crates/rustpatcher/e2e_test/keys/owner_key_win new file mode 100644 index 0000000..03d66bb --- /dev/null +++ b/crates/rustpatcher/e2e_test/keys/owner_key_win @@ -0,0 +1 @@ +1swm1b9affb4c59c8yh69wey4axji7fhyjnwhtmnce69m8tspfmy \ No newline at end of file From ba91ce1d934ac762fe175919be3f65fc6c38c600 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 01:03:14 +0200 Subject: [PATCH 6/9] added supported platform table + dissabled windows e2e test --- .github/workflows/e2e-test.yml | 10 +++++----- README.md | 22 +++++++++++++++------- crates/rustpatcher/README.md | 20 ++++++++++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 33ea106..10541ab 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -27,11 +27,11 @@ jobs: binary_ext: "" key_file: owner_key_macos timeout_cmd: gtimeout - - os: windows-latest - platform: win - binary_ext: ".exe" - key_file: owner_key_win - timeout_cmd: timeout + #- os: windows-latest + # platform: win + # binary_ext: ".exe" + # key_file: owner_key_win + # timeout_cmd: timeout runs-on: ${{ matrix.os }} timeout-minutes: 30 diff --git a/README.md b/README.md index 2ac04fe..c99e765 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ # Rust Patcher Secure fully decentralized software updates. +## Supported Platforms + +| Platform | Architecture | Supported | +|----------|--------------|-----------| +| Linux | x86_64 | Yes | +| Linux | ARM64 | Yes | +| macOS | x86_64 | Yes | +| macOS | ARM64 | Yes | +| Windows | - | Not yet | + +**Note:** windows support will follow, *windows build err: libc is not available in nix pkg* ## Implementation Flow @@ -30,12 +41,9 @@ async fn main() -> anyhow::Result<()> { println!("my version is {:?}", rustpatcher::Version::current()?); // your app code after this - loop { - tokio::select! { - _ = tokio::signal::ctrl_c() => { - println!("Exiting on Ctrl-C"); - break; - } + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("Exiting on Ctrl-C"); } } Ok(()) @@ -229,4 +237,4 @@ build: publish: target/release/ -``` +``` \ No newline at end of file diff --git a/crates/rustpatcher/README.md b/crates/rustpatcher/README.md index d13b99d..c99e765 100644 --- a/crates/rustpatcher/README.md +++ b/crates/rustpatcher/README.md @@ -5,6 +5,17 @@ # Rust Patcher Secure fully decentralized software updates. +## Supported Platforms + +| Platform | Architecture | Supported | +|----------|--------------|-----------| +| Linux | x86_64 | Yes | +| Linux | ARM64 | Yes | +| macOS | x86_64 | Yes | +| macOS | ARM64 | Yes | +| Windows | - | Not yet | + +**Note:** windows support will follow, *windows build err: libc is not available in nix pkg* ## Implementation Flow @@ -30,12 +41,9 @@ async fn main() -> anyhow::Result<()> { println!("my version is {:?}", rustpatcher::Version::current()?); // your app code after this - loop { - tokio::select! { - _ = tokio::signal::ctrl_c() => { - println!("Exiting on Ctrl-C"); - break; - } + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("Exiting on Ctrl-C"); } } Ok(()) From 1c98f7f944513ea75b3a3015775c80867cefd01a Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 01:04:11 +0200 Subject: [PATCH 7/9] fixed windows e2e test deactivation --- .github/workflows/e2e-test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 10541ab..ae23c54 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] include: - os: ubuntu-latest platform: linux @@ -27,11 +27,11 @@ jobs: binary_ext: "" key_file: owner_key_macos timeout_cmd: gtimeout - #- os: windows-latest - # platform: win - # binary_ext: ".exe" - # key_file: owner_key_win - # timeout_cmd: timeout + - os: windows-latest + platform: win + binary_ext: ".exe" + key_file: owner_key_win + timeout_cmd: timeout runs-on: ${{ matrix.os }} timeout-minutes: 30 From 65de4092881273a6a9b9a8dcf8933148ee002e53 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 01:06:30 +0200 Subject: [PATCH 8/9] ghactions removed summary --- .github/workflows/e2e-test.yml | 36 ++++++---------------------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index ae23c54..421684f 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -27,11 +27,11 @@ jobs: binary_ext: "" key_file: owner_key_macos timeout_cmd: gtimeout - - os: windows-latest - platform: win - binary_ext: ".exe" - key_file: owner_key_win - timeout_cmd: timeout + #- os: windows-latest + # platform: win + # binary_ext: ".exe" + # key_file: owner_key_win + # timeout_cmd: timeout runs-on: ${{ matrix.os }} timeout-minutes: 30 @@ -254,28 +254,4 @@ jobs: new_output.txt old_error.txt new_error.txt - retention-days: 7 - - - name: Test Summary - if: success() - run: | - echo "E2E update test completed successfully for ${{ matrix.platform }}" - echo "- Built and signed version ${{ steps.version.outputs.old_version }}" - echo "- Built and signed version ${{ steps.version.outputs.new_version }}" - echo "- Old version successfully updated itself via P2P to new version" - echo "- Update mechanism working correctly!" - shell: bash - - summary: - needs: build-and-test - runs-on: ubuntu-latest - if: always() - steps: - - name: Check test results - run: | - if [ "${{ needs.build-and-test.result }}" = "success" ]; then - echo "All E2E tests passed across all platforms!" - else - echo "Some E2E tests failed. Check the logs above." - exit 1 - fi + retention-days: 7 \ No newline at end of file From b0d34def3f6c80aa9d6689ef2fd46ead527232a9 Mon Sep 17 00:00:00 2001 From: rustonbsd Date: Sun, 12 Oct 2025 01:11:23 +0200 Subject: [PATCH 9/9] fixed version v0.2.1 --- crates/rustpatcher/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rustpatcher/Cargo.toml b/crates/rustpatcher/Cargo.toml index 44dd8fe..f9e9238 100644 --- a/crates/rustpatcher/Cargo.toml +++ b/crates/rustpatcher/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpatcher" -version = "0.2.2" +version = "0.2.1" edition = "2024" description = "distributed patching system for single binary applications" license = "MIT"