//! Geprüfte Runtime-Vorlagen, Nutzlastcontainer und geschützte Veröffentlichung. use anyhow::{bail, ensure, Context, Result}; use std::{ fs, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, }; use tb_vm::bytecode::{CompiledModule, TBC_VERSION}; pub const RUNTIME_VERSION: u32 = 1; const CONTAINER_VERSION: u32 = 1; const MAGIC: &[u8; 8] = b"TBPCODE!"; const FOOTER: usize = 48; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u32)] pub enum Target { WindowsAmd64 = 1, MacosArm64 = 2, LinuxAmd64 = 3, LinuxArm64 = 4, } impl Target { pub const ALL: [Self; 4] = [ Self::WindowsAmd64, Self::MacosArm64, Self::LinuxAmd64, Self::LinuxArm64, ]; pub fn triple(self) -> &'static str { match self { Self::WindowsAmd64 => "x86_64-pc-windows-msvc", Self::MacosArm64 => "aarch64-apple-darwin", Self::LinuxAmd64 => "x86_64-unknown-linux-gnu", Self::LinuxArm64 => "aarch64-unknown-linux-gnu", } } pub fn parse(s: &str) -> Result { Self::ALL .into_iter() .find(|t| t.triple() == s) .with_context(|| format!("Nicht unterstütztes Target: {s}")) } pub fn host() -> Result { match (std::env::consts::OS, std::env::consts::ARCH) { ("windows", "x86_64") => Ok(Self::WindowsAmd64), ("macos", "aarch64") => Ok(Self::MacosArm64), ("linux", "x86_64") => Ok(Self::LinuxAmd64), ("linux", "aarch64") => Ok(Self::LinuxArm64), other => bail!("Kein unterstütztes Host-Target: {other:?}"), } } pub fn runtime_name(self) -> &'static str { if self == Self::WindowsAmd64 { "tbrt.exe" } else { "tbrt" } } } fn part(b: &[u8], at: usize, len: usize) -> Result<&[u8]> { b.get(at..at.checked_add(len).context("Längenüberlauf")?) .context("Abgeschnittenes natives Format/Nutzlast") } fn u16_at(b: &[u8], at: usize) -> Result { Ok(u16::from_le_bytes(part(b, at, 2)?.try_into()?)) } fn u32_at(b: &[u8], at: usize) -> Result { Ok(u32::from_le_bytes(part(b, at, 4)?.try_into()?)) } fn u64_at(b: &[u8], at: usize) -> Result { Ok(u64::from_le_bytes(part(b, at, 8)?.try_into()?)) } fn size_at(b: &[u8], at: usize) -> Result { usize::try_from(u64_at(b, at)?).context("Nutzlastlänge nicht darstellbar") } fn put64(b: &mut [u8], at: usize, n: usize) { b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes()); } /// FNV-1a-64 gegen Übertragungs-/Dateibeschädigung; keine Herkunftsauthentisierung. pub fn checksum(b: &[u8]) -> u64 { b.iter().fold(0xcbf29ce484222325, |h, v| { (h ^ u64::from(*v)).wrapping_mul(0x100000001b3) }) } fn macho_commands(b: &[u8]) -> Result> { let end = 32usize .checked_add(u32_at(b, 20)? as usize) .context("Mach-O-Headerlänge")?; part(b, 0, end)?; let mut at = 32; let mut commands = Vec::new(); for _ in 0..u32_at(b, 16)? { ensure!(at + 8 <= end, "Ungültige Mach-O-Ladekommandos"); let cmd = u32_at(b, at)?; let len = u32_at(b, at + 4)? as usize; ensure!( len >= 8 && len.is_multiple_of(8) && len <= end - at, "Ungültige Mach-O-Kommandolänge" ); commands.push((cmd, at)); at += len; } ensure!(at == end, "Mach-O-Kommandotabelle unvollständig"); Ok(commands) } /// Format-/Architekturprüfung; synthetische Header sind kein Zielausführungsnachweis. pub fn native_target(b: &[u8]) -> Result { part(b, 0, 64)?; if b.starts_with(b"MZ") { let pe = u32_at(b, 0x3c)? as usize; ensure!(part(b, pe, 4)? == b"PE\0\0", "Ungültiges PE-Format"); ensure!( u16_at(b, pe + 4)? == 0x8664, "PE-Architektur muss amd64 sein" ); ensure!( u16_at(b, pe + 24)? == 0x20b && u16_at(b, pe + 92)? == 3, "PE muss PE32+ Terminalanwendung sein" ); ensure!( u16_at(b, pe + 22)? & 0x2002 == 2, "PE muss Executable statt DLL sein" ); return Ok(Target::WindowsAmd64); } if b.starts_with(b"\x7fELF") { ensure!( matches!(b[7], 0 | 3), "ELF-System-ABI muss System V/Linux sein" ); ensure!( part(b, 4, 3)? == [2, 1, 1], "ELF muss 64-Bit Little Endian sein" ); ensure!(matches!(u16_at(b, 16)?, 2 | 3), "ELF muss ausführbar sein"); part(b, 0, 64)?; return match u16_at(b, 18)? { 62 => Ok(Target::LinuxAmd64), 183 => Ok(Target::LinuxArm64), _ => bail!("Nicht unterstützte ELF-Architektur"), }; } if b.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) { ensure!( u32_at(b, 4)? == 0x0100000c, "Mach-O-Architektur muss arm64 sein" ); ensure!(u32_at(b, 12)? == 2, "Mach-O muss Executable sein"); macho_commands(b)?; return Ok(Target::MacosArm64); } bail!("Unbekanntes natives Zielformat (PE/ELF/Mach-O erwartet)") } /// Mach-O-Signaturen folgen der Nutzlast. Ihr Ladekommando ist die stabile Grenze. fn content_end(b: &[u8], target: Target) -> Result { if target == Target::MacosArm64 { let signatures: Vec<_> = macho_commands(b)? .into_iter() .filter(|(cmd, _)| *cmd == 0x1d) .collect(); ensure!(signatures.len() <= 1, "Doppelte Mach-O-Signatur"); if let Some((_, at)) = signatures.first() { ensure!(u32_at(b, at + 4)? == 16, "Ungültiges Signaturkommando"); let off = u32_at(b, at + 8)? as usize; let len = u32_at(b, at + 12)? as usize; part(b, off, len)?; ensure!(off + len == b.len(), "Daten hinter Mach-O-Signatur"); return Ok(off); } } Ok(b.len()) } pub fn embedded(b: &[u8], expected: Target) -> Result { ensure!( native_target(b)? == expected, "Executable-Target passt nicht zum Host" ); let end = content_end(b, expected)?; // codesign richtet die Signatur auf 16 Bytes aus; ausschließlich Nullpadding zulassen. let footer = (0..=15) .find_map(|padding| { let at = end.checked_sub(FOOTER + padding)?; (b.get(at..at + 8) == Some(MAGIC) && b[at + FOOTER..end].iter().all(|v| *v == 0)) .then_some(at) }) .context("Fehlende oder beschädigte eingebettete Nutzlast (leere tbrt-Vorlage)")?; ensure!( u32_at(b, footer + 8)? == CONTAINER_VERSION, "Inkompatible Container-Version" ); ensure!( u32_at(b, footer + 12)? == RUNTIME_VERSION, "Inkompatible Runtime-Version" ); ensure!( u32_at(b, footer + 16)? == u32::from(TBC_VERSION), "Inkompatible TBC-Version" ); ensure!( u32_at(b, footer + 20)? == expected as u32, "Nutzlast-Target passt nicht zum Executable" ); let offset = size_at(b, footer + 24)?; let len = size_at(b, footer + 32)?; ensure!( offset >= 64 && offset <= footer && len == footer - offset, "Ungültige Nutzlastgrenzen" ); let payload = part(b, offset, len)?; ensure!( checksum(payload) == u64_at(b, footer + 40)?, "Nutzlast-Prüfsumme stimmt nicht" ); CompiledModule::from_tbc(payload) .map_err(|e| anyhow::anyhow!("Eingebettetes Programm nicht ladbar: {e}")) } /// Im nativen Runner vorhanden; verhindert Verwechslung mit beliebigen Executables. pub const fn runtime_marker(target: Target) -> [u8; 32] { let mut marker = [0; 32]; let magic = *b"TBRT-RUNTIME-v1!"; let mut i = 0; while i < 16 { marker[i] = magic[i]; i += 1; } let values = [ CONTAINER_VERSION, RUNTIME_VERSION, TBC_VERSION as u32, target as u32, ]; i = 0; while i < 4 { let bytes = values[i].to_le_bytes(); let mut j = 0; while j < 4 { marker[16 + i * 4 + j] = bytes[j]; j += 1; } i += 1; } marker } fn check_runner(b: &[u8], target: Target) -> Result<()> { ensure!( b.windows(32).any(|w| w == runtime_marker(target)), "Keine kompatible tbrt-Runtime-Vorlage (Runtime-/Format-/Target-Markierung fehlt)" ); Ok(()) } fn manifest(b: &[u8], target: Target) -> String { format!("TBRT-TEMPLATE 1\nruntime={RUNTIME_VERSION}\npackage={}\ntbc={TBC_VERSION}\ntarget={}\nchecksum={:016x}\n",env!("CARGO_PKG_VERSION"),target.triple(),checksum(b)) } pub fn manifest_path(path: &Path) -> PathBuf { let mut p = path.as_os_str().to_owned(); p.push(".meta"); PathBuf::from(p) } /// Für den Runtime-Builder; kein impliziter Build beim Export. pub fn prepare_template(path: &Path, target: Target) -> Result<()> { let b = fs::read(path)?; check_runner(&b, target)?; ensure!( native_target(&b)? == target, "Vorlagen-Zielformat/Architektur widerspricht {}", target.triple() ); ensure!( embedded(&b, target).is_err(), "Bereits eingebettetes Programm ist keine leere Vorlage" ); publish( &manifest_path(path), false, &[path.to_path_buf()], &|| false, |temp| { fs::write(temp, manifest(&b, target))?; Ok(()) }, ) } fn template(path: &Path, target: Target) -> Result> { let b = fs::read(path).with_context(|| format!("Runtime-Vorlage fehlt: {}", path.display()))?; ensure!( native_target(&b)? == target, "Vorlagen-Zielformat/Architektur passt nicht zu {}", target.triple() ); let found = fs::read_to_string(manifest_path(path)).context("Vorlagenmetadaten fehlen")?; check_runner(&b, target)?; let want = manifest(&b, target); for (index, (a, z)) in found.lines().zip(want.lines()).enumerate() { ensure!( a == z, "Inkompatible Vorlagenmetadaten, Zeile {}: Soll {z}, Ist {a}", index + 1 ); } ensure!( found == want, "Unvollständige Vorlagenmetadaten oder Integritätsdaten" ); Ok(b) } static NEXT: AtomicU64 = AtomicU64::new(0); struct Temporary(PathBuf); impl Drop for Temporary { fn drop(&mut self) { let _ = fs::remove_file(&self.0); } } fn destination(path: &Path, protected: &[PathBuf]) -> Result>> { let key = tb_vm::project_io::identity(path)?; for source in protected { ensure!( tb_vm::project_io::identity(source)? != key, "Ausgabe würde Projektdaten/Vorlage überschreiben: {}", path.display() ); } match fs::symlink_metadata(path) { Ok(meta) => { ensure!( meta.is_file() && !meta.permissions().readonly(), "Ziel ist keine beschreibbare reguläre Datei: {}", path.display() ); Ok(Some(fs::read(path)?)) } Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), } } /// No-clobber per hard_link; Ersetzen nur nach expliziter Freigabe und unverändertem Ziel. /// prepare darf finalisieren; cancellation wird vor Arbeit und Veröffentlichung geprüft. pub fn publish( path: &Path, overwrite: bool, protected: &[PathBuf], cancelled: &dyn Fn() -> bool, prepare: impl FnOnce(&Path) -> Result<()>, ) -> Result<()> { ensure!(!cancelled(), "Export abgebrochen"); let initial = destination(path, protected)?; ensure!( initial.is_none() || overwrite, "Ziel existiert; --force zum Ersetzen: {}", path.display() ); let parent = path .parent() .filter(|p| !p.as_os_str().is_empty()) .unwrap_or(Path::new(".")); let temporary = loop { let p = parent.join(format!( ".tb-export-{}-{}.tmp", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); match fs::OpenOptions::new().write(true).create_new(true).open(&p) { Ok(_) => break Temporary(p), Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, Err(e) => return Err(e.into()), } }; prepare(&temporary.0)?; fs::File::open(&temporary.0)?.sync_all()?; ensure!(!cancelled(), "Export abgebrochen"); ensure!( destination(path, protected)? == initial, "Ziel wurde während des Exports verändert" ); if initial.is_some() { fs::rename(&temporary.0, path)?; } else { fs::hard_link(&temporary.0, path) .context("Ziel inzwischen belegt oder Veröffentlichung nicht möglich")?; } Ok(()) } fn codesign(path: &Path, args: &[&str]) -> Result<()> { ensure!( cfg!(target_os = "macos"), "macOS-Finalisierung benötigt macOS mit /usr/bin/codesign" ); let result = std::process::Command::new("/usr/bin/codesign") .args(args) .arg(path) .output() .context("Finalisierung: /usr/bin/codesign fehlt oder ist nicht ausführbar")?; ensure!( result.status.success(), "Finalisierung fehlgeschlagen: {}", String::from_utf8_lossy(&result.stderr) ); Ok(()) } fn append_payload(b: &mut Vec, module: &CompiledModule, target: Target) -> Result<()> { module.validate().map_err(|e| anyhow::anyhow!("{e}"))?; if target == Target::WindowsAmd64 { let pe = u32_at(b, 0x3c)? as usize; // Eine bestehende Authenticode-Signatur wäre nach dem Einbetten ungültig. if u32_at(b, pe + 132)? > 4 { ensure!( part(b, pe + 168, 8)?.iter().all(|v| *v == 0), "Signierte PE-Vorlagen werden nicht unterstützt; unsigned tbrt benötigt" ); } // Für Benutzerprogramme optional; keine veraltete PE-Prüfsumme übernehmen. b[pe + 88..pe + 92].fill(0); } let payload = module.to_tbc(); let offset = b.len(); b.extend_from_slice(&payload); b.extend_from_slice(MAGIC); for n in [ CONTAINER_VERSION, RUNTIME_VERSION, u32::from(TBC_VERSION), target as u32, ] { b.extend_from_slice(&n.to_le_bytes()); } for n in [offset as u64, payload.len() as u64, checksum(&payload)] { b.extend_from_slice(&n.to_le_bytes()); } if target == Target::MacosArm64 { let link = macho_commands(b)? .into_iter() .find(|(cmd, at)| { *cmd == 0x19 && b.get(at + 8..at + 24) == Some(b"__LINKEDIT\0\0\0\0\0\0") }) .context("Mach-O __LINKEDIT fehlt")? .1; ensure!( u32_at(b, link + 4)? as usize >= 72, "Mach-O-Segment zu kurz" ); let start = size_at(b, link + 40)?; ensure!(start <= offset, "Ungültige __LINKEDIT-Grenze"); let len = b.len() - start; put64(b, link + 48, len); put64( b, link + 32, len.checked_add(16383).context("Mach-O-Länge")? & !16383, ); } Ok(()) } pub struct Export<'a> { pub module: &'a CompiledModule, pub target: Target, pub template: &'a Path, pub output: &'a Path, pub overwrite: bool, pub protected: &'a [PathBuf], } pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> { let mut b = template(request.template, request.target)?; let mut protected = request.protected.to_vec(); protected.extend([ request.template.to_path_buf(), manifest_path(request.template), ]); protected.extend( request .module .sources .iter() .map(|s| PathBuf::from(&s.path)), ); publish( request.output, request.overwrite, &protected, cancelled, |temp| { if request.target == Target::MacosArm64 { fs::write(temp, &b)?; codesign(temp, &["--remove-signature"])?; b = fs::read(temp)?; } append_payload(&mut b, request.module, request.target)?; fs::write(temp, &b)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(temp, fs::Permissions::from_mode(0o755))?; } if request.target == Target::MacosArm64 { codesign(temp, &["--force", "--sign", "-", "--timestamp=none"])?; codesign(temp, &["--verify", "--strict"])?; } embedded(&fs::read(temp)?, request.target)?; Ok(()) }, ) } #[cfg(test)] mod tests;