515 lines
17 KiB
Rust
515 lines
17 KiB
Rust
//! 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> {
|
|
Self::ALL
|
|
.into_iter()
|
|
.find(|t| t.triple() == s)
|
|
.with_context(|| format!("Nicht unterstütztes Target: {s}"))
|
|
}
|
|
pub fn host() -> Result<Self> {
|
|
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<u16> {
|
|
Ok(u16::from_le_bytes(part(b, at, 2)?.try_into()?))
|
|
}
|
|
fn u32_at(b: &[u8], at: usize) -> Result<u32> {
|
|
Ok(u32::from_le_bytes(part(b, at, 4)?.try_into()?))
|
|
}
|
|
fn u64_at(b: &[u8], at: usize) -> Result<u64> {
|
|
Ok(u64::from_le_bytes(part(b, at, 8)?.try_into()?))
|
|
}
|
|
fn size_at(b: &[u8], at: usize) -> Result<usize> {
|
|
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<Vec<(u32, usize)>> {
|
|
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<Target> {
|
|
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<usize> {
|
|
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<CompiledModule> {
|
|
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<Vec<u8>> {
|
|
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<Option<Vec<u8>>> {
|
|
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<u8>, 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;
|