#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_possible_truncation)]
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::Context;
use clap::{Parser, Subcommand};
#[cfg(feature = "script")]
mod script;
#[derive(Parser, Debug)]
#[command(
name = "ud",
version,
about = "univdreams: a universal compiler/decompiler suite",
long_about = "Decompile binaries to a directive-rich C-like source language and \
recompile to byte-identical binaries."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Roundtrip {
input: PathBuf,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
through_source: bool,
},
Decompile {
input: PathBuf,
#[arg(short, long)]
out: Option<PathBuf>,
},
Verify {
input: PathBuf,
},
Compile {
input: PathBuf,
#[arg(short, long)]
out: Option<PathBuf>,
},
Analyze {
input: PathBuf,
#[arg(long, default_value_t = 5_000_000)]
max_instructions: u64,
#[arg(long)]
json: bool,
#[arg(long)]
monitor: bool,
#[arg(long, allow_hyphen_values = true)]
args: Option<String>,
#[arg(long)]
dump_vfs: Option<PathBuf>,
#[arg(long)]
kvm: bool,
#[arg(long)]
net: bool,
#[arg(short = 'i', long)]
interactive: bool,
#[arg(long, value_name = "TYPE")]
fs_type: Option<String>,
#[arg(long, value_name = "POINT=SPEC")]
mount: Vec<String>,
#[arg(long)]
no_default_mounts: bool,
#[arg(long, value_name = "SPEC")]
rootfs: Option<String>,
#[arg(long, default_value_t = true)]
preserve_deletes: bool,
#[arg(long)]
fail_soft: bool,
#[arg(long, value_name = "DIR")]
stage_vfs: Vec<PathBuf>,
#[arg(long)]
vfs_deps: bool,
#[arg(long, value_name = "PATTERN")]
call_export: Vec<String>,
#[arg(long, value_name = "N")]
export_arg: Vec<String>,
#[arg(long = "call", value_name = "NAME[:args]")]
call: Vec<String>,
#[arg(long, value_name = "SPEC")]
dump_mem: Vec<String>,
#[arg(long, value_name = "SPEC")]
map_blob: Vec<String>,
#[arg(long, value_name = "MiB", default_value_t = univdreams::emulator::Sandbox::DEFAULT_HEAP_MB)]
heap_mb: u32,
#[arg(long, value_name = "ADDR:LEN")]
watch: Vec<String>,
},
Vfw {
#[command(subcommand)]
command: VfwCommand,
},
Qtcodec {
#[command(subcommand)]
command: QtcodecCommand,
},
Solana {
program_id: String,
#[arg(long, default_value = univdreams::cli::solana::DEFAULT_RPC)]
rpc: String,
#[arg(long)]
no_cache: bool,
#[arg(long)]
save_elf: Option<PathBuf>,
#[arg(short, long)]
out: Option<PathBuf>,
},
#[cfg(feature = "script")]
Script {
file: PathBuf,
#[arg(long, value_name = "MiB", default_value_t = univdreams::emulator::Sandbox::DEFAULT_HEAP_MB)]
heap_mb: u32,
#[arg(long, default_value_t = 100_000_000)]
max_instructions: u64,
},
}
#[derive(Subcommand, Debug)]
enum QtcodecCommand {
Register {
codec: PathBuf,
#[arg(long)]
export: String,
#[arg(long)]
ty: String,
#[arg(long)]
subtype: String,
#[arg(long, default_value = "appl")]
manufacturer: String,
#[arg(long, value_name = "DIR")]
stage_vfs: Vec<PathBuf>,
#[arg(long, default_value_t = 1_000_000_000)]
max_instructions: u64,
},
List {
#[arg(long, default_value = "imdc")]
ty: String,
#[arg(long, default_value = "0")]
subtype: String,
#[arg(long, default_value = "0")]
manufacturer: String,
#[arg(long, value_name = "DIR")]
stage_vfs: Vec<PathBuf>,
#[arg(long, value_name = "MSI")]
install_msi: Vec<PathBuf>,
#[arg(long, default_value_t = 1_000_000_000)]
max_instructions: u64,
},
Call {
dll: PathBuf,
#[arg(long)]
export: String,
#[arg(long, value_name = "U32", value_parser = parse_u32)]
arg: Vec<u32>,
#[arg(long, value_name = "DIR")]
stage_vfs: Vec<PathBuf>,
#[arg(long, default_value_t = 200_000_000)]
max_instructions: u64,
},
Dispatch {
codec: PathBuf,
#[arg(long)]
export: String,
#[arg(long, default_value_t = -4)]
selector: i16,
#[arg(long, value_name = "U32", value_parser = parse_u32)]
param: Vec<u32>,
#[arg(long, default_value_t = 0, value_parser = parse_u32)]
storage: u32,
#[arg(long, value_name = "DIR")]
stage_vfs: Vec<PathBuf>,
#[arg(long, default_value_t = 100_000_000)]
max_instructions: u64,
},
}
#[derive(Subcommand, Debug)]
enum VfwCommand {
Probe {
dll: PathBuf,
#[arg(long = "fcc-handler", value_name = "FCC")]
fcc_handler: Option<String>,
#[arg(long = "pix-format", value_enum, default_value_t = PixFormat::Rgb24)]
pix_format: PixFormat,
#[arg(long, default_value_t = 320)]
width: u32,
#[arg(long, default_value_t = 240)]
height: u32,
#[arg(long, default_value_t = 100_000_000)]
max_instructions: u64,
},
Decode {
dll: PathBuf,
#[arg(long, value_name = "FILE", required = true)]
input: Vec<PathBuf>,
#[arg(long)]
width: u32,
#[arg(long)]
height: u32,
#[arg(long = "fcc-handler", value_name = "FCC")]
fcc_handler: Option<String>,
#[arg(long = "pix-format", value_enum, default_value_t = PixFormat::Rgb24)]
pix_format: PixFormat,
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(long, default_value_t = 100_000_000)]
max_instructions: u64,
#[arg(long, value_name = "SPEC")]
dump_mem: Vec<String>,
#[arg(long, value_name = "N")]
dump_after_frame: Option<usize>,
#[arg(long, value_name = "MiB", default_value_t = univdreams::emulator::Sandbox::DEFAULT_HEAP_MB)]
heap_mb: u32,
#[arg(long, value_name = "ADDR:LEN")]
watch: Vec<String>,
},
Encode {
dll: PathBuf,
#[arg(long, value_name = "FILE")]
input: PathBuf,
#[arg(long)]
width: u32,
#[arg(long)]
height: u32,
#[arg(long = "fcc-handler", value_name = "FCC")]
fcc_handler: Option<String>,
#[arg(long = "input-format", value_enum, default_value_t = InputFormat::Bgr24)]
input_format: InputFormat,
#[arg(long, default_value_t = 5000)]
quality: u32,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
keyframe: bool,
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(long, default_value_t = 100_000_000)]
max_instructions: u64,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
enum PixFormat {
Rgb24,
Rgb32,
Yuv,
}
impl PixFormat {
fn bi_bit_count(self) -> u16 {
match self {
PixFormat::Rgb24 => 24,
PixFormat::Rgb32 => 32,
PixFormat::Yuv => 16,
}
}
fn bi_compression(self) -> [u8; 4] {
match self {
PixFormat::Rgb24 | PixFormat::Rgb32 => [0; 4],
PixFormat::Yuv => *b"YUY2",
}
}
fn bytes_per_pixel(self) -> u32 {
match self {
PixFormat::Rgb24 => 3,
PixFormat::Rgb32 => 4,
PixFormat::Yuv => 2,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
enum InputFormat {
Bgr24,
Bgr32,
Yv12,
I420,
Yuy2,
}
impl InputFormat {
fn bi_bit_count(self) -> u16 {
match self {
InputFormat::Bgr24 => 24,
InputFormat::Bgr32 => 32,
InputFormat::Yv12 | InputFormat::I420 => 12,
InputFormat::Yuy2 => 16,
}
}
fn bi_compression(self) -> [u8; 4] {
match self {
InputFormat::Bgr24 | InputFormat::Bgr32 => [0; 4],
InputFormat::Yv12 => *b"YV12",
InputFormat::I420 => *b"I420",
InputFormat::Yuy2 => *b"YUY2",
}
}
fn frame_bytes(self, width: u32, height: u32) -> u32 {
let pixels = width.saturating_mul(height);
match self {
InputFormat::Bgr24 => pixels * 3,
InputFormat::Bgr32 => pixels * 4,
InputFormat::Yv12 | InputFormat::I420 => pixels * 3 / 2,
InputFormat::Yuy2 => pixels * 2,
}
}
}
fn main() -> ExitCode {
univdreams::translate::register_all_arches();
let cli = Cli::parse();
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("ud: {err:#}");
ExitCode::FAILURE
}
}
}
#[allow(clippy::too_many_lines)]
fn run(cli: Cli) -> anyhow::Result<()> {
match cli.command {
Command::Roundtrip {
input,
out,
through_source,
} => {
let output = out.unwrap_or_else(|| {
let mut p = input.clone().into_os_string();
p.push(".rebuilt");
PathBuf::from(p)
});
if through_source {
let report = univdreams::cli::roundtrip_through_source(&input, &output)
.with_context(|| {
format!(
"source round-trip from {} to {}",
input.display(),
output.display()
)
})?;
for w in &report.warnings {
eprintln!("warning: {}", format_warning(w));
}
if !report.warnings.is_empty() {
eprintln!("({} verify warning(s))", report.warnings.len());
}
if report.byte_identical {
println!(
"source round-trip ok: {} == {} ({} bytes)",
input.display(),
output.display(),
report.input_len,
);
} else {
let offset = report
.first_diff_offset
.map_or("?".into(), |o| format!("0x{o:x}"));
println!(
"source round-trip differs: {} != {} (input {} bytes, output {} bytes; first diff at {})",
input.display(),
output.display(),
report.input_len,
report.output_len,
offset,
);
if let Some(ctx) = &report.diff_context {
eprintln!(
"input @ 0x{:x}: {}",
ctx.window_start,
hex_window(&ctx.input_window)
);
eprintln!(
"output @ 0x{:x}: {}",
ctx.window_start,
hex_window(&ctx.output_window)
);
}
}
Ok(())
} else {
univdreams::cli::roundtrip(&input, &output).with_context(|| {
format!(
"round-trip from {} to {}",
input.display(),
output.display()
)
})?;
println!("round-trip ok: {} == {}", input.display(), output.display());
Ok(())
}
}
Command::Decompile { input, out } => {
let bytes =
std::fs::read(&input).with_context(|| format!("read {}", input.display()))?;
let source = if univdreams::format::elf::is_elf64_le(&bytes) {
let elf = univdreams::format::elf::Elf64File::parse(&bytes)
.with_context(|| format!("parse {} as ELF", input.display()))?;
univdreams::translate::decompile::decompile_to_text(&elf)
.with_context(|| format!("decompile {}", input.display()))?
} else if univdreams::format::ne::is_ne(&bytes) {
let ne = univdreams::format::ne::NeFile::parse(&bytes)
.with_context(|| format!("parse {} as NE", input.display()))?;
univdreams::translate::decompile::decompile_ne_to_text(&ne)
} else if univdreams::format::pe::is_pe(&bytes) {
let pe = univdreams::format::pe::PeFile::parse(&bytes)
.with_context(|| format!("parse {} as PE", input.display()))?;
univdreams::translate::decompile::decompile_pe_to_text(&pe)
} else if univdreams::format::macho::is_macho64(&bytes) {
let macho = univdreams::format::macho::MachoFile::parse(&bytes)
.with_context(|| format!("parse {} as Mach-O", input.display()))?;
univdreams::translate::decompile::decompile_macho_to_text(&macho)
} else if univdreams::format::wasm::is_wasm(&bytes) {
let wasm = univdreams::format::wasm::WasmFile::parse(&bytes)
.with_context(|| format!("parse {} as WASM", input.display()))?;
univdreams::translate::decompile::decompile_wasm_to_text(&wasm)
} else if let Some(load_addr) = univdreams::cli::raw_6502_load_addr(&bytes) {
let image = univdreams::format::raw::RawImage::new(bytes, load_addr);
univdreams::translate::decompile::decompile_raw_6502_to_text(&image)
.with_context(|| format!("decompile {} as 6502 raw", input.display()))?
} else {
anyhow::bail!(
"unrecognised binary format: {} (expected ELF, PE, NE, Mach-O, WASM, or 6502 raw image)",
input.display()
);
};
if let Some(path) = out {
std::fs::write(&path, source)
.with_context(|| format!("write {}", path.display()))?;
} else {
use std::io::Write as _;
std::io::stdout().write_all(source.as_bytes())?;
}
Ok(())
}
Command::Verify { input } => {
let text = std::fs::read_to_string(&input)
.with_context(|| format!("read {}", input.display()))?;
let ast = univdreams::translate::compile::parse(&text)
.with_context(|| format!("parse {}", input.display()))?;
let warnings = univdreams::translate::compile::verify_asm(&ast);
if warnings.is_empty() {
println!(
"ok: {} ({} item{})",
input.display(),
ast.items.len(),
if ast.items.len() == 1 { "" } else { "s" }
);
return Ok(());
}
for w in &warnings {
eprintln!("{}", format_warning(w));
}
eprintln!("{} warning(s) in {}", warnings.len(), input.display());
Ok(())
}
Command::Compile { input, out } => {
let output = out.unwrap_or_else(|| {
let mut p = input.clone().into_os_string();
p.push(".bin");
PathBuf::from(p)
});
let text = std::fs::read_to_string(&input)
.with_context(|| format!("read {}", input.display()))?;
let ast = univdreams::translate::compile::parse(&text)
.with_context(|| format!("parse {}", input.display()))?;
let format = ast
.module
.fields
.iter()
.find(|f| f.name == "format")
.and_then(|f| match &f.value {
univdreams::ast::Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| {
anyhow::anyhow!("`@module.format` is missing — expected \"elf\", \"pe\", \"ne\", \"macho\", \"wasm\", or \"raw\"")
})?;
let bytes = match format.as_str() {
"elf" => univdreams::translate::compile::lower_to_elf(&ast)
.with_context(|| format!("lower {} to ELF", input.display()))?,
"pe" => univdreams::translate::compile::lower_to_pe(&ast)
.with_context(|| format!("lower {} to PE", input.display()))?,
"ne" => univdreams::translate::compile::lower_to_ne(&ast)
.with_context(|| format!("lower {} to NE", input.display()))?,
"macho" => univdreams::translate::compile::lower_to_macho(&ast)
.with_context(|| format!("lower {} to Mach-O", input.display()))?,
"wasm" => univdreams::translate::compile::lower_to_wasm(&ast)
.with_context(|| format!("lower {} to WASM", input.display()))?,
"raw" => univdreams::translate::compile::lower_to_raw(&ast)
.with_context(|| format!("lower {} to raw", input.display()))?,
other => anyhow::bail!(
"unsupported `@module.format` value {other:?} (expected \"elf\", \"pe\", \"ne\", \"macho\", \"wasm\", or \"raw\")"
),
};
std::fs::write(&output, &bytes)
.with_context(|| format!("write {}", output.display()))?;
println!(
"compiled: {} → {} ({} bytes, format: {})",
input.display(),
output.display(),
bytes.len(),
format,
);
Ok(())
}
Command::Analyze {
input,
max_instructions,
json,
monitor,
args,
dump_vfs,
kvm,
net,
interactive,
fs_type,
mount,
no_default_mounts,
rootfs,
preserve_deletes,
fail_soft,
stage_vfs,
vfs_deps,
call_export,
export_arg,
call,
dump_mem,
map_blob,
heap_mb,
watch,
} => {
if monitor {
monitor_install(
&input,
max_instructions,
json,
args.as_deref(),
dump_vfs.as_deref(),
preserve_deletes,
kvm,
net,
interactive,
&FsOpts {
fs_type,
mounts: mount,
no_default_mounts,
rootfs,
},
)
} else {
analyze(
&input,
max_instructions,
json,
fail_soft || vfs_deps,
&stage_vfs,
vfs_deps,
&call_export,
&export_arg,
&call,
&dump_mem,
&map_blob,
heap_mb,
&watch,
)
}
}
Command::Solana {
program_id,
rpc,
no_cache,
save_elf,
out,
} => solana_cmd(
&program_id,
&rpc,
!no_cache,
save_elf.as_deref(),
out.as_deref(),
),
Command::Vfw { command } => match command {
VfwCommand::Probe {
dll,
fcc_handler,
pix_format,
width,
height,
max_instructions,
} => vfw_probe(
&dll,
fcc_handler.as_deref(),
pix_format,
width,
height,
max_instructions,
),
VfwCommand::Decode {
dll,
input,
width,
height,
fcc_handler,
pix_format,
output,
max_instructions,
dump_mem,
dump_after_frame,
heap_mb,
watch,
} => decode_cmd(
&dll,
&input,
width,
height,
fcc_handler.as_deref(),
pix_format,
output.as_deref(),
max_instructions,
&dump_mem,
dump_after_frame,
heap_mb,
&watch,
),
VfwCommand::Encode {
dll,
input,
width,
height,
fcc_handler,
input_format,
quality,
keyframe,
output,
max_instructions,
} => encode_cmd(
&dll,
&input,
width,
height,
fcc_handler.as_deref(),
input_format,
quality,
keyframe,
output.as_deref(),
max_instructions,
),
},
Command::Qtcodec { command } => match command {
QtcodecCommand::Register {
codec,
export,
ty,
subtype,
manufacturer,
stage_vfs,
max_instructions,
} => qtcodec_register(
&codec,
&export,
&ty,
&subtype,
&manufacturer,
&stage_vfs,
max_instructions,
),
QtcodecCommand::List {
ty,
subtype,
manufacturer,
stage_vfs,
install_msi,
max_instructions,
} => qtcodec_list(
&ty,
&subtype,
&manufacturer,
&stage_vfs,
&install_msi,
max_instructions,
),
QtcodecCommand::Call {
dll,
export,
arg,
stage_vfs,
max_instructions,
} => qtcodec_call(&dll, &export, &arg, &stage_vfs, max_instructions),
QtcodecCommand::Dispatch {
codec,
export,
selector,
param,
storage,
stage_vfs,
max_instructions,
} => qtcodec_dispatch(
&codec,
&export,
selector,
¶m,
storage,
&stage_vfs,
max_instructions,
),
},
#[cfg(feature = "script")]
Command::Script {
file,
heap_mb,
max_instructions,
} => script::run_script(&file, heap_mb, max_instructions),
}
}
fn fourcc_to_u32(s: &str) -> u32 {
let mut b = [b' '; 4];
for (i, c) in s.bytes().take(4).enumerate() {
b[i] = c;
}
u32::from_le_bytes(b)
}
fn derive_default_fcc(p: &Path) -> String {
let stem = p
.file_stem()
.map(|s| s.to_string_lossy().to_ascii_uppercase())
.unwrap_or_default();
if stem.contains("IR32") {
"IV31".into()
} else if stem.contains("IR41") {
"IV41".into()
} else if stem.contains("IR50") {
"IV50".into()
} else if stem.contains("CVID") || stem.contains("ICCVID") {
"cvid".into()
} else if stem.contains("MPG4C32") || stem.contains("MPG4") {
"MP43".into()
} else {
"IV31".into()
}
}
const ICMODE_DECOMPRESS: u32 = 1;
const ICMODE_COMPRESS: u32 = 2;
const ICCOMPRESS_KEYFRAME: u32 = 0x0000_0001;
const QTCODEC_SCRATCH: u32 = 0x65FE_0000;
fn ensure_cm_hashtable(sandbox: &mut univdreams::emulator::Sandbox) {
const HASHTABLE_GLOBAL: u32 = 0x6738_9e44;
const CM_INIT_OFFSET: u32 = 0x9_2600;
let qts_base = sandbox
.host
.modules
.get("quicktime.qts")
.copied()
.unwrap_or(0x6680_0000);
if qts_base == 0 {
return;
}
let before = sandbox.mmu.load32(HASHTABLE_GLOBAL).unwrap_or(0);
if before != 0 {
return; }
let init_fn = qts_base.wrapping_add(CM_INIT_OFFSET);
eprintln!(
" qts CM hashtable @ {HASHTABLE_GLOBAL:#010x} is NULL — \
invoking qts!CM_init at {init_fn:#010x}"
);
sandbox.host.exit_requested = None;
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
init_fn,
&[],
) {
Ok(v) => eprintln!(" qts!CM_init() = {v:#010x}"),
Err(e) => eprintln!(" qts!CM_init trapped: {e}"),
}
let after = sandbox.mmu.load32(HASHTABLE_GLOBAL).unwrap_or(0);
eprintln!(" qts CM hashtable @ {HASHTABLE_GLOBAL:#010x} = {after:#010x} (after init)");
}
fn init_qtml(sandbox: &mut univdreams::emulator::Sandbox) -> u32 {
let Some(target) = sandbox.registry.resolve("qtmlclient.dll", "InitializeQTML") else {
eprintln!("init_qtml: qtmlclient!InitializeQTML not in registry — pre-load missed?");
return u32::MAX;
};
sandbox.host.exit_requested = None;
let stub_before = sandbox.host.stub_calls.len();
let result = univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[0],
);
let calls = &sandbox.host.stub_calls[stub_before..];
if !calls.is_empty() {
eprintln!("--- {} stub calls during InitializeQTML ---", calls.len());
for c in calls.iter().take(40) {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({}) -> {:#x}",
c.dll,
c.name,
args.join(", "),
c.ret
);
}
if calls.len() > 40 {
eprintln!(" … ({} more)", calls.len() - 40);
}
}
match result {
Ok(v) => {
eprintln!("InitializeQTML(0) = {v:#x}");
v
}
Err(e) => {
eprintln!(
"InitializeQTML(0) trapped: {e} (eip={:#010x})",
sandbox.cpu.regs.eip
);
u32::MAX
}
}
}
fn preload_qt_runtime(sandbox: &mut univdreams::emulator::Sandbox) {
for runtime_dll in &["qtmlclient.dll", "quicktime.qts"] {
let path = sandbox.context().vfs.as_ref().and_then(|v| {
for prefix in univdreams::emulator::win32::dll_vfs_search_paths() {
let p = format!("{prefix}{runtime_dll}");
if v.read(&p).is_some() {
return Some(p);
}
}
None
});
let Some(path) = path else { continue };
let dll_bytes = sandbox
.context()
.vfs
.as_ref()
.and_then(|v| v.read(&path))
.map(<[u8]>::to_vec);
let Some(dll_bytes) = dll_bytes else { continue };
match sandbox.load_with_deps(runtime_dll, &dll_bytes) {
Ok((img, _)) => {
eprintln!(
"pre-loaded {runtime_dll} at {:#010x} ({} exports)",
img.image_base,
img.exports.len()
);
for (name, rva) in &img.exports {
sandbox.registry.register_guest_export(
runtime_dll,
name,
img.image_base.wrapping_add(*rva),
);
}
let stub_before = sandbox.host.stub_calls.len();
match sandbox.call_dll_main(&img, univdreams::emulator::DLL_PROCESS_ATTACH) {
Ok(rc) => eprintln!(" {runtime_dll}: DllMain returned {rc:#x}"),
Err(e) => eprintln!(
" {runtime_dll}: DllMain trapped: {e} (eip={:#010x})",
sandbox.cpu.regs.eip
),
}
let calls = &sandbox.host.stub_calls[stub_before..];
if !calls.is_empty() {
eprintln!(
" --- {} stub calls during {runtime_dll} DllMain ---",
calls.len()
);
let n = calls.len();
let head = 200;
let tail = 200;
let log_call = |c: &univdreams::emulator::win32::StubCall| {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({}) -> {:#x}",
c.dll,
c.name,
args.join(", "),
c.ret
);
};
if n <= head + tail {
for c in calls {
log_call(c);
}
} else {
for c in calls.iter().take(head) {
log_call(c);
}
eprintln!(" … ({} more, last {} below)", n - head - tail, tail);
for c in calls.iter().skip(n - tail) {
log_call(c);
}
}
}
}
Err(e) => eprintln!("pre-load {runtime_dll}: {e}"),
}
}
}
fn fourcc_be(s: &str) -> u32 {
if let Ok(v) = parse_u32(s)
&& v == 0
{
return 0;
}
let mut b = [b' '; 4];
for (i, c) in s.bytes().take(4).enumerate() {
b[i] = c;
}
u32::from_be_bytes(b)
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn qtcodec_register(
codec: &Path,
export: &str,
ty: &str,
subtype: &str,
manufacturer: &str,
stage_vfs: &[PathBuf],
max_instructions: u64,
) -> anyhow::Result<()> {
let codec_bytes = std::fs::read(codec).with_context(|| format!("read {}", codec.display()))?;
let codec_stem = codec
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("codec.qtx");
let ty_v = fourcc_be(ty);
let sub_v = fourcc_be(subtype);
let man_v = fourcc_be(manufacturer);
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
for d in stage_vfs {
let n = stage_dir_into_vfs(sandbox.context_mut(), d)
.with_context(|| format!("stage {}", d.display()))?;
eprintln!("staged {n} files from {}", d.display());
}
preload_qt_runtime(&mut sandbox);
let init_rc = init_qtml(&mut sandbox);
if init_rc != 0 {
anyhow::bail!("InitializeQTML failed: {init_rc:#x}");
}
if let Some(target) = sandbox.registry.resolve("qtmlclient.dll", "EnterMovies") {
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[],
) {
Ok(v) => eprintln!("EnterMovies() = {v:#x}"),
Err(e) => eprintln!("EnterMovies() trapped: {e}"),
}
}
ensure_cm_hashtable(&mut sandbox);
let (image, _) = sandbox
.load_with_deps(codec_stem, &codec_bytes)
.with_context(|| format!("load {}", codec.display()))?;
eprintln!("loaded {codec_stem} at {:#010x}", image.image_base);
if let Err(e) = sandbox.call_dll_main(&image, univdreams::emulator::DLL_PROCESS_ATTACH) {
eprintln!("{codec_stem}: DllMain trapped: {e}");
}
let Some(entry_rva) = image.exports.get(export) else {
anyhow::bail!("codec doesn't export {export}");
};
let entry_va = image.image_base.wrapping_add(*entry_rva);
eprintln!("dispatch entry {export} @ {entry_va:#010x}");
let desc_addr = QTCODEC_SCRATCH + 0x10000;
let words = [ty_v, sub_v, man_v, 0, 0];
for (i, w) in words.iter().enumerate() {
sandbox
.mmu
.store32(desc_addr + u32::try_from(i).unwrap_or(0) * 4, *w)
.map_err(|e| anyhow::anyhow!("write CD[{i}]: {e}"))?;
}
eprintln!(
"ComponentDescription @ {desc_addr:#x}: type={ty_v:#x} subType={sub_v:#x} mfr={man_v:#x}"
);
let Some(target) = sandbox
.registry
.resolve("qtmlclient.dll", "RegisterComponent")
else {
anyhow::bail!("qtmlclient!RegisterComponent missing");
};
let result = univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[desc_addr, entry_va, 0, 0, 0, 0],
);
match result {
Ok(v) => println!("RegisterComponent = {v:#010x} (component handle)"),
Err(e) => {
eprintln!("RegisterComponent trapped: {e}");
return Err(anyhow::anyhow!("{e}"));
}
}
let Some(target) = sandbox
.registry
.resolve("qtmlclient.dll", "CountComponents")
else {
anyhow::bail!("CountComponents missing");
};
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[desc_addr],
) {
Ok(v) => println!("CountComponents after register = {v}"),
Err(e) => eprintln!("CountComponents trapped: {e}"),
}
let found = sandbox
.registry
.resolve("qtmlclient.dll", "FindNextComponent")
.and_then(|target| {
univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[0, desc_addr],
)
.ok()
})
.unwrap_or(0);
println!("FindNextComponent(NULL, &desc) = {found:#010x}");
{
eprintln!("qts dispatch_table — non-default entries:");
let cat0_fn = sandbox.mmu.load32(0x6735_6240 + 4).unwrap_or(0);
for cat in 0..0x100u32 {
let a = 0x6735_6240u32.wrapping_add(cat * 8);
let enabled = sandbox.mmu.load8(a).unwrap_or(0);
let fn_ptr = sandbox.mmu.load32(a + 4).unwrap_or(0);
if enabled != 0 || (fn_ptr != cat0_fn && fn_ptr != 0) {
eprintln!(
" cat[0x{cat:02x}] @ {a:#010x}: enabled=0x{enabled:02x} fn_ptr={fn_ptr:#010x}"
);
}
}
}
{
let v0 = sandbox.mmu.load32(0x67dd_c000).unwrap_or(0);
let vc = sandbox.mmu.load32(0x67dd_c00c).unwrap_or(0);
let tls_off = sandbox.mmu.load32(0x6734_7000).unwrap_or(0);
let tls_idx = sandbox.mmu.load32(0x6734_7004).unwrap_or(0);
eprintln!(
"codec vtable: [0x67ddc000]={v0:#010x} \
[0x67ddc00c]={vc:#010x} (pre-Open)"
);
eprintln!(
"qts TLS: [0x67347000]={tls_off:#010x} \
[0x67347004]={tls_idx:#010x} (pre-Open)"
);
}
{
let recs = sandbox.mmu.load32(0x6734_a4ec).unwrap_or(0);
if recs != 0 {
let arr = sandbox.mmu.load32(recs.wrapping_add(0x18)).unwrap_or(0);
let pool0 = sandbox.mmu.load32(recs).unwrap_or(0);
if arr != 0 && pool0 != 0 {
let slot50 = sandbox.mmu.load32(arr.wrapping_add(0x50)).unwrap_or(0);
eprintln!(" [record[0]+0x50]: 0x{slot50:08x} pool[0]: 0x{pool0:08x}");
}
}
}
if found != 0
&& let Some(target) = sandbox.registry.resolve("qtmlclient.dll", "OpenComponent")
{
let before = sandbox.host.stub_calls.len();
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[found],
) {
Ok(v) => println!("OpenComponent({found:#010x}) = {v:#010x}"),
Err(e) => eprintln!("OpenComponent trapped: {e}"),
}
{
let v0 = sandbox.mmu.load32(0x67dd_c000).unwrap_or(0);
let vc = sandbox.mmu.load32(0x67dd_c00c).unwrap_or(0);
eprintln!(
"codec vtable: [0x67ddc000]={v0:#010x} \
[0x67ddc00c]={vc:#010x} (post-Open)"
);
let recs_ptr = sandbox.mmu.load32(0x6734_a4ec).unwrap_or(0);
if recs_ptr != 0 {
let inst_cnt = sandbox.mmu.load16(recs_ptr.wrapping_add(0x0c)).unwrap_or(0);
let inst_lim = sandbox.mmu.load16(recs_ptr.wrapping_add(0x0e)).unwrap_or(0);
let count = sandbox.mmu.load16(recs_ptr.wrapping_add(0x14)).unwrap_or(0);
let arr_ptr = sandbox.mmu.load32(recs_ptr.wrapping_add(0x18)).unwrap_or(0);
let pool = sandbox.mmu.load32(recs_ptr.wrapping_add(0x20)).unwrap_or(0);
eprintln!(
"qts records: ptr={recs_ptr:#010x} \
inst_cnt=0x{inst_cnt:04x} inst_lim=0x{inst_lim:04x} \
count=0x{count:04x} arr=0x{arr_ptr:08x} pool=0x{pool:08x}"
);
if arr_ptr != 0 {
eprintln!(" record[0] @ {arr_ptr:#010x}:");
for row in 0..7u32 {
let off = arr_ptr.wrapping_add(row * 16);
let bytes: Vec<String> = (0..16u32)
.map(|i| format!("{:02x}", sandbox.mmu.load8(off + i).unwrap_or(0)))
.collect();
eprintln!(" +{:#04x}: {}", row * 16, bytes.join(" "));
}
}
}
}
let calls = &sandbox.host.stub_calls[before..];
eprintln!("--- {} stub calls during OpenComponent ---", calls.len());
let n = calls.len();
let head = 60;
let tail = 60;
let log_call = |c: &univdreams::emulator::win32::StubCall| {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({}) -> {:#x}",
c.dll,
c.name,
args.join(", "),
c.ret
);
};
if n <= head + tail {
for c in calls {
log_call(c);
}
} else {
for c in calls.iter().take(head) {
log_call(c);
}
eprintln!(" … ({} more, last {} below)", n - head - tail, tail);
for c in calls.iter().skip(n - tail) {
log_call(c);
}
}
}
Ok(())
}
#[allow(
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::unreadable_literal
)]
fn qtcodec_list(
ty: &str,
subtype: &str,
manufacturer: &str,
stage_vfs: &[PathBuf],
install_msi: &[PathBuf],
max_instructions: u64,
) -> anyhow::Result<()> {
let ty_v = fourcc_be(ty);
let sub_v = fourcc_be(subtype);
let man_v = fourcc_be(manufacturer);
let mut staged_vfs = univdreams::emulator::context::VirtualFs::new();
let mut staged_reg = univdreams::emulator::context::VirtualRegistry::new();
{
let mut tmp_ctx = univdreams::emulator::context::Context {
vfs: Some(univdreams::emulator::fsmount::MountTable::new()),
..univdreams::emulator::context::Context::default()
};
for d in stage_vfs {
let n = stage_dir_into_vfs(&mut tmp_ctx, d)
.with_context(|| format!("stage {}", d.display()))?;
eprintln!("staged {n} files from {}", d.display());
}
if let Some(v) = tmp_ctx.vfs.take() {
for (p, _) in v.list() {
if let Some(b) = v.read(p) {
staged_vfs.insert(p, b.to_vec());
}
}
}
}
if !install_msi.is_empty() {
let mut install_sb = univdreams::emulator::Sandbox::new();
install_sb.host.trace_stubs = true;
install_sb.host.instruction_budget = Some(max_instructions);
install_sb.context_mut().vfs = Some(univdreams::emulator::fsmount::MountTable::with_root(
staged_vfs.clone(),
));
install_sb.context_mut().registry = Some(staged_reg.clone());
for msi_path in install_msi {
let bytes =
std::fs::read(msi_path).with_context(|| format!("read {}", msi_path.display()))?;
let stage_key = format!(
"c:/temp/install_{}.msi",
msi_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("pkg")
);
if let Some(vfs) = install_sb.context_mut().vfs.as_mut() {
vfs.insert(&stage_key, bytes);
}
let synth_cmd = format!("msiexec.exe /i \"{stage_key}\"");
univdreams::emulator::win32::msiexec::dispatch_msiexec_install(
&mut install_sb.host,
&mut install_sb.mmu,
&stage_key,
&synth_cmd,
);
let pumped = install_sb.pump_pending_msi_install();
eprintln!(
"installed {} — pumped {pumped} deferred CustomAction(s)",
msi_path.display()
);
}
if let Some(v) = install_sb.context().vfs.as_ref() {
for (p, _) in v.list() {
if let Some(b) = v.read(p) {
staged_vfs.insert(p, b.to_vec());
}
}
}
if let Some(r) = install_sb.context().registry.as_ref() {
for (k, n, v) in r.all_values() {
staged_reg.set_value(k, n, v.clone());
}
}
}
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox.context_mut().vfs = Some(univdreams::emulator::fsmount::MountTable::with_root(
staged_vfs,
));
sandbox.context_mut().registry = Some(staged_reg);
preload_qt_runtime(&mut sandbox);
let init_rc = init_qtml(&mut sandbox);
if init_rc != 0 {
anyhow::bail!("InitializeQTML failed: {init_rc:#x}");
}
if let Some(target) = sandbox.registry.resolve("qtmlclient.dll", "EnterMovies") {
sandbox.host.exit_requested = None;
let before = sandbox.host.stub_calls.len();
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[],
) {
Ok(v) => eprintln!("EnterMovies() = {v:#x}"),
Err(e) => eprintln!(
"EnterMovies() trapped: {e} (eip={:#010x})",
sandbox.cpu.regs.eip
),
}
let calls = &sandbox.host.stub_calls[before..];
eprintln!("--- {} stub calls during EnterMovies ---", calls.len());
for c in calls.iter().take(60) {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({}) -> {:#x}",
c.dll,
c.name,
args.join(", "),
c.ret
);
}
if calls.len() > 60 {
eprintln!(" … ({} more)", calls.len() - 60);
}
}
ensure_cm_hashtable(&mut sandbox);
let desc_addr = QTCODEC_SCRATCH + 0x10000;
let words = [ty_v, sub_v, man_v, 0, 0];
for (i, w) in words.iter().enumerate() {
sandbox
.mmu
.store32(desc_addr + u32::try_from(i).unwrap_or(0) * 4, *w)
.map_err(|e| anyhow::anyhow!("write CD[{i}]: {e}"))?;
}
eprintln!(
"ComponentDescription @ {desc_addr:#x}: type={ty_v:#x} subType={sub_v:#x} mfr={man_v:#x}"
);
let Some(target) = sandbox
.registry
.resolve("qtmlclient.dll", "CountComponents")
else {
anyhow::bail!("qtmlclient!CountComponents not in registry");
};
let stub_before = sandbox.host.stub_calls.len();
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[desc_addr],
) {
Ok(v) => {
println!("CountComponents(type={ty:?}, subType={subtype:?}) = {v}");
}
Err(e) => {
eprintln!("CountComponents trapped: {e}");
}
}
let calls = &sandbox.host.stub_calls[stub_before..];
eprintln!("--- {} stub calls during CountComponents ---", calls.len());
for c in calls.iter().take(30) {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let arg_str = args.join(", ");
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({arg_str}) -> {:#x}",
c.dll, c.name, c.ret
);
}
if let Some(target) = sandbox
.registry
.resolve("qtmlclient.dll", "OpenADefaultComponent")
{
let stub_before = sandbox.host.stub_calls.len();
sandbox.host.exit_requested = None;
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[ty_v, sub_v],
) {
Ok(v) => println!("OpenADefaultComponent({ty:?}, {subtype:?}) = {v:#x}"),
Err(e) => eprintln!("OpenADefaultComponent trapped: {e}"),
}
let calls = &sandbox.host.stub_calls[stub_before..];
eprintln!(
"--- {} stub calls during OpenADefaultComponent ---",
calls.len()
);
for c in calls.iter().take(20) {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({}) -> {:#x}",
c.dll,
c.name,
args.join(", "),
c.ret
);
}
}
if let Some(target) = sandbox
.registry
.resolve("qtmlclient.dll", "FindNextComponent")
{
let stub_before = sandbox.host.stub_calls.len();
match univdreams::emulator::win32::call_guest(
&mut sandbox.cpu,
&mut sandbox.mmu,
&mut sandbox.registry,
&mut sandbox.host,
target,
&[0, desc_addr], ) {
Ok(v) => println!("FindNextComponent(NULL, &desc) = {v:#x}"),
Err(e) => eprintln!("FindNextComponent trapped: {e}"),
}
let calls = &sandbox.host.stub_calls[stub_before..];
eprintln!(
"--- {} stub calls during FindNextComponent ---",
calls.len()
);
for c in calls.iter().take(30) {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let arg_str = args.join(", ");
let eip = c.call_site_eip;
eprintln!(
" {eip:#010x} {}!{}({arg_str}) -> {:#x}",
c.dll, c.name, c.ret
);
}
}
let probe_addrs = &[
(0x6735_6240u32, "dispatch_table[cat 0..3] (32 bytes)"),
(0x6738_6860u32, "CM global state struct (32 bytes)"),
(0x6734_a4ecu32, "[CM global ptr]"),
(0x6734_a4e0u32, "[QT dispatcher ptr]"),
(0x6688_45b0u32, "theQuickTimeDispatcher prologue (32 bytes)"),
(0x6735_6248u32, "cat[1] entry (8 bytes raw)"),
(0x1004_dcd0u32, "qtmlclient init flag (4 bytes)"),
(
0x1004_dcdcu32,
"qtmlclient->theQuickTimeDispatcher (4 bytes)",
),
(0x1002_4220u32, "qtmlclient!RegisterComponent (16 bytes)"),
(0x6688_4890u32, "cat[1].subtable stub (16 bytes)"),
(0x6734_7000u32, "qts CRT-init flag (4 bytes)"),
(0x6734_7004u32, "qts TLS slot index (4 bytes)"),
(0x6738_51e8u32, "qts thread-data lock (4 bytes)"),
(0x6738_51ecu32, "qts thread-data list head (4 bytes)"),
(0x7FFD_DFF0u32, "page below TEB (32 bytes)"),
(0x7FFD_E000u32, "TEB start (32 bytes)"),
(
0x4003_80a0u32,
"libdispatch IAT slot range (32 bytes incl _initterm)",
),
(
0x4003_80e0u32,
"libdispatch IAT slot range (32 bytes incl pthread_setspecific @ +0x10)",
),
(
0x4000_6750u32,
"pthread_setspecific impl prologue (32 bytes)",
),
(
0x4000_c160u32,
"pthread internal state slot [+0xc164] (16 bytes)",
),
(0x4000_6700u32, "pthread inner helper (32 bytes)"),
(0x4003_a498u32, "libdispatch loaded value slot (32 bytes)"),
(0x4003_a480u32, "libdispatch around 0xa498 (32 bytes)"),
(0x6000_a810u32, "heap-allocated pthread_key_t (32 bytes)"),
];
eprintln!("--- runtime CM state ---");
for (a, label) in probe_addrs {
let mut bytes = [0u8; 32];
let mut ok = true;
for (i, slot) in bytes.iter_mut().enumerate() {
if let Ok(b) = sandbox.mmu.load8(a.wrapping_add(u32::try_from(i).unwrap())) {
*slot = b;
} else {
ok = false;
break;
}
}
if ok {
let hex: String = bytes
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ");
eprintln!(" 0x{a:08x} ({label}): {hex}");
} else {
eprintln!(" 0x{a:08x} ({label}): UNMAPPED");
}
}
Ok(())
}
fn qtcodec_call(
dll: &Path,
export: &str,
args: &[u32],
stage_vfs: &[PathBuf],
max_instructions: u64,
) -> anyhow::Result<()> {
let bytes = std::fs::read(dll).with_context(|| format!("read {}", dll.display()))?;
let stem = dll.file_name().and_then(|s| s.to_str()).unwrap_or("qtdll");
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
for d in stage_vfs {
let n = stage_dir_into_vfs(sandbox.context_mut(), d)
.with_context(|| format!("stage {}", d.display()))?;
eprintln!("staged {n} files from {}", d.display());
}
preload_qt_runtime(&mut sandbox);
let (image, _) = sandbox
.load_with_deps(stem, &bytes)
.with_context(|| format!("load_with_deps {}", dll.display()))?;
eprintln!("loaded {} at {:#010x}", stem, image.image_base);
match sandbox.call_dll_main(&image, univdreams::emulator::DLL_PROCESS_ATTACH) {
Ok(v) => eprintln!("DllMain returned {v:#x}"),
Err(e) => eprintln!("DllMain trapped: {e}"),
}
if export != "InitializeQTML" {
init_qtml(&mut sandbox);
}
eprintln!("calling {export}({args:?})");
sandbox.host.stub_calls.clear();
let res = sandbox.call_export(&image, export, args);
let calls = std::mem::take(&mut sandbox.host.stub_calls);
match res {
Ok(ret) => {
#[allow(clippy::cast_possible_wrap)]
let signed = ret as i32;
println!("{export}: ret = {ret:#010x} ({signed})");
}
Err(e) => {
eprintln!("{export}: trapped: {e}");
}
}
eprintln!("--- {} stub calls during {export} ---", calls.len());
for c in &calls {
let args: Vec<String> = c.args.iter().map(|a| format!("{a:#x}")).collect();
let eip = c.call_site_eip;
let arg_str = args.join(", ");
eprintln!(
" {eip:#010x} {}!{}({arg_str}) -> {:#x}",
c.dll, c.name, c.ret
);
}
for line in &sandbox.host.debug_log {
eprintln!(" {line}");
}
Ok(())
}
fn parse_u32(s: &str) -> Result<u32, String> {
let s = s.trim();
if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u32::from_str_radix(rest, 16).map_err(|e| e.to_string())
} else {
s.parse::<u32>().map_err(|e| e.to_string())
}
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
fn qtcodec_dispatch(
codec: &Path,
export: &str,
selector: i16,
params: &[u32],
storage_size: u32,
stage_vfs: &[PathBuf],
max_instructions: u64,
) -> anyhow::Result<()> {
let bytes = std::fs::read(codec).with_context(|| format!("read {}", codec.display()))?;
let stem = codec
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("codec");
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
for dir in stage_vfs {
let n = stage_dir_into_vfs(sandbox.context_mut(), dir)
.with_context(|| format!("stage {}", dir.display()))?;
eprintln!("staged {n} files from {}", dir.display());
}
preload_qt_runtime(&mut sandbox);
let (image, _unresolved) = sandbox
.load_with_deps(stem, &bytes)
.with_context(|| format!("load_with_deps {}", codec.display()))?;
eprintln!("loaded {} at {:#010x}", stem, image.image_base);
match sandbox.call_dll_main(&image, univdreams::emulator::DLL_PROCESS_ATTACH) {
Ok(v) => eprintln!("DllMain returned {v:#x}"),
Err(e) => eprintln!("DllMain trapped: {e}"),
}
let cp_addr = QTCODEC_SCRATCH;
let param_bytes: u8 = u8::try_from(params.len() * 4).unwrap_or(u8::MAX);
let what_bits: u16 = u16::from_le_bytes(selector.to_le_bytes());
sandbox
.mmu
.store8(cp_addr, 0)
.map_err(|e| anyhow::anyhow!("write CP flags: {e}"))?;
sandbox
.mmu
.store8(cp_addr + 1, param_bytes)
.map_err(|e| anyhow::anyhow!("write CP paramSize: {e}"))?;
sandbox
.mmu
.store16(cp_addr + 2, what_bits)
.map_err(|e| anyhow::anyhow!("write CP what: {e}"))?;
for (i, w) in params.iter().enumerate() {
sandbox
.mmu
.store32(cp_addr + 4 + u32::try_from(i).unwrap_or(0) * 4, *w)
.map_err(|e| anyhow::anyhow!("write CP param {i}: {e}"))?;
}
let storage: u32 = if storage_size == 0 {
0
} else {
let handle_addr = QTCODEC_SCRATCH + 0x0001_0000;
let data_addr = handle_addr + 4;
sandbox
.mmu
.store32(handle_addr, data_addr)
.map_err(|e| anyhow::anyhow!("write handle master: {e}"))?;
for off in 0..storage_size {
sandbox
.mmu
.store8(data_addr + off, 0)
.map_err(|e| anyhow::anyhow!("zero storage[{off}]: {e}"))?;
}
eprintln!(
"minted handle at {handle_addr:#x} → {storage_size}-byte block at {data_addr:#x}"
);
handle_addr
};
let storage_label = if storage == 0 {
"NULL".to_string()
} else {
format!("{storage:#x}")
};
eprintln!(
"calling {export}(cp={cp_addr:#x} selector={selector}, params={params:?}, storage={storage_label})"
);
match sandbox.call_export(&image, export, &[cp_addr, storage]) {
Ok(ret) => {
#[allow(clippy::cast_possible_wrap)]
let signed = ret as i32;
println!("{export}: ret = {ret:#010x} ({signed})");
}
Err(e) => {
eprintln!("{export}: trapped: {e}");
return Err(anyhow::anyhow!("{e}"));
}
}
for line in &sandbox.host.debug_log {
eprintln!(" {line}");
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn vfw_probe(
dll_path: &Path,
fcc_handler: Option<&str>,
pix_format: PixFormat,
width: u32,
height: u32,
max_instructions: u64,
) -> anyhow::Result<()> {
let dll_bytes =
std::fs::read(dll_path).with_context(|| format!("reading {}", dll_path.display()))?;
let dll_name = dll_path
.file_name()
.map_or_else(|| "codec.dll".into(), |n| n.to_string_lossy().into_owned());
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.instruction_budget = Some(max_instructions);
sandbox.host.trace_stubs = true;
let img = sandbox
.load(&dll_name, &dll_bytes)
.with_context(|| format!("load {}", dll_path.display()))?;
let _ = sandbox
.call_dll_main(&img, univdreams::emulator::DLL_PROCESS_ATTACH)
.with_context(|| "DllMain")?;
sandbox
.install_codec(&img)
.with_context(|| "install_codec")?;
let fcc = fcc_handler.map_or_else(|| derive_default_fcc(dll_path), str::to_owned);
let fcc_type = u32::from_le_bytes(*b"VIDC");
let fcc_handler_u32 = fourcc_to_u32(&fcc);
let hic = sandbox
.ic_open(fcc_type, fcc_handler_u32, ICMODE_DECOMPRESS)
.context("ICOpen(ICMODE_DECOMPRESS)")?;
if hic == 0 {
anyhow::bail!("codec refused DRV_OPEN");
}
println!(
"[probe] loaded {} (image_base 0x{:x})",
dll_path.display(),
img.image_base
);
println!("[probe] HIC = {hic}; fcc_handler = {fcc:?}");
match sandbox.ic_get_info(hic, 568) {
Ok(info) => {
let read_u32 = |off: usize| -> u32 {
if off + 4 <= info.len() {
u32::from_le_bytes(info[off..off + 4].try_into().unwrap_or([0; 4]))
} else {
0
}
};
let read_utf16 = |off: usize, max_chars: usize| -> String {
let mut out = String::new();
for i in 0..max_chars {
let o = off + i * 2;
if o + 2 > info.len() {
break;
}
let c = u16::from_le_bytes([info[o], info[o + 1]]);
if c == 0 {
break;
}
if let Some(ch) = char::from_u32(u32::from(c)) {
out.push(ch);
}
}
out
};
let fcc_type_bytes = read_u32(4).to_le_bytes();
let fcc_handler_bytes = read_u32(8).to_le_bytes();
let flags = read_u32(12);
let version = read_u32(16);
let version_icm = read_u32(20);
let name = read_utf16(24, 16);
let desc = read_utf16(24 + 32, 128);
println!("[probe] ICINFO:");
println!(
" fccType = {:?}",
std::str::from_utf8(&fcc_type_bytes).unwrap_or("?")
);
println!(
" fccHandler = {:?}",
std::str::from_utf8(&fcc_handler_bytes).unwrap_or("?")
);
println!(" dwFlags = 0x{flags:08x}");
println!(" dwVersion = 0x{version:08x}");
println!(" dwVersionICM = 0x{version_icm:08x}");
println!(" szName = {name:?}");
println!(" szDescription= {desc:?}");
}
Err(e) => {
println!("[probe] ICGetInfo failed: {e}");
}
}
let in_bih = univdreams::emulator::Bih {
bi_size: 40,
width: width as i32,
height: height as i32,
planes: 1,
bit_count: 24,
compression: fcc_handler_u32.to_le_bytes(),
size_image: 0,
..univdreams::emulator::Bih::default()
};
let out_bih = univdreams::emulator::Bih {
bi_size: 40,
width: width as i32,
height: height as i32,
planes: 1,
bit_count: pix_format.bi_bit_count(),
compression: pix_format.bi_compression(),
size_image: width * height * pix_format.bytes_per_pixel(),
..univdreams::emulator::Bih::default()
};
match sandbox.ic_decompress_query(hic, &in_bih, Some(&out_bih)) {
Ok(q) => println!(
"[probe] ICDecompressQuery({}x{} {:?} → {:?}) = {} (0 = ICERR_OK)",
width, height, fcc, pix_format, q as i32
),
Err(e) => println!("[probe] ICDecompressQuery failed: {e}"),
}
let _ = sandbox.ic_close(hic);
println!();
println!(
"[probe] Win32 calls: {}; instructions executed: {}",
sandbox.host.stub_calls.len(),
sandbox.host.instructions_executed
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
fn decode_cmd(
dll_path: &Path,
inputs: &[PathBuf],
width: u32,
height: u32,
fcc_handler: Option<&str>,
pix_format: PixFormat,
output: Option<&Path>,
max_instructions: u64,
dump_mem: &[String],
dump_after_frame: Option<usize>,
heap_mb: u32,
watch: &[String],
) -> anyhow::Result<()> {
let dump_specs: Vec<DumpSpec> = dump_mem
.iter()
.map(|s| parse_dump_spec(s).with_context(|| format!("--dump-mem {s:?}")))
.collect::<anyhow::Result<_>>()?;
let watch_specs: Vec<(u32, u32)> = watch
.iter()
.map(|s| parse_watch_spec(s).with_context(|| format!("--watch {s:?}")))
.collect::<anyhow::Result<_>>()?;
let dll_bytes =
std::fs::read(dll_path).with_context(|| format!("reading {}", dll_path.display()))?;
let dll_name = dll_path
.file_name()
.map_or_else(|| "codec.dll".into(), |n| n.to_string_lossy().into_owned());
let frames: Vec<Vec<u8>> = inputs
.iter()
.map(|p| std::fs::read(p).with_context(|| format!("reading frame {}", p.display())))
.collect::<anyhow::Result<_>>()?;
let mut sandbox = univdreams::emulator::Sandbox::new_with_heap_mb(heap_mb);
sandbox.host.instruction_budget = Some(max_instructions);
let img = sandbox
.load(&dll_name, &dll_bytes)
.with_context(|| format!("load {}", dll_path.display()))?;
let _ = sandbox
.call_dll_main(&img, univdreams::emulator::DLL_PROCESS_ATTACH)
.with_context(|| "DllMain")?;
sandbox
.install_codec(&img)
.with_context(|| "install_codec")?;
let fcc = fcc_handler.map_or_else(|| derive_default_fcc(dll_path), str::to_owned);
let fcc_type = u32::from_le_bytes(*b"VIDC");
let fcc_handler_u32 = fourcc_to_u32(&fcc);
let in_bih = univdreams::emulator::Bih {
bi_size: 40,
width: width as i32,
height: height as i32,
planes: 1,
bit_count: 24,
compression: fcc_handler_u32.to_le_bytes(),
size_image: u32::try_from(frames[0].len()).unwrap_or(u32::MAX),
..univdreams::emulator::Bih::default()
};
let out_bih = univdreams::emulator::Bih {
bi_size: 40,
width: width as i32,
height: height as i32,
planes: 1,
bit_count: pix_format.bi_bit_count(),
compression: pix_format.bi_compression(),
size_image: width * height * pix_format.bytes_per_pixel(),
..univdreams::emulator::Bih::default()
};
let hic = sandbox
.ic_open(fcc_type, fcc_handler_u32, ICMODE_DECOMPRESS)
.context("ICOpen(ICMODE_DECOMPRESS)")?;
if hic == 0 {
anyhow::bail!("codec refused DRV_OPEN");
}
eprintln!("[decode] HIC = {hic}; fcc_handler = {fcc:?}");
let q = sandbox
.ic_decompress_query(hic, &in_bih, Some(&out_bih))
.context("ICDecompressQuery")?;
eprintln!("[decode] ICDecompressQuery = {} (0 = ICERR_OK)", q as i32);
if (q as i32) != 0 {
anyhow::bail!("codec rejected the in/out BIH pair");
}
let _ = sandbox.ic_decompress_begin(hic, &in_bih, &out_bih);
let out_capacity = width * height * pix_format.bytes_per_pixel();
for &(addr, len) in &watch_specs {
sandbox.mmu.add_watch(addr, len);
}
let dump_idx = match dump_after_frame {
Some(n) if (1..=frames.len()).contains(&n) => n - 1,
Some(n) => {
eprintln!(
"warning: --dump-after-frame {n} out of range 1..={}; dumping after the last frame",
frames.len()
);
frames.len() - 1
}
None => frames.len() - 1,
};
let mut last_decoded: Vec<u8> = Vec::new();
for (i, frame) in frames.iter().enumerate() {
let frame_bih = univdreams::emulator::Bih {
bi_size: 40,
width: width as i32,
height: height as i32,
planes: 1,
bit_count: 24,
compression: fcc_handler_u32.to_le_bytes(),
size_image: u32::try_from(frame.len()).unwrap_or(u32::MAX),
..univdreams::emulator::Bih::default()
};
let (rc, decoded) = sandbox
.ic_decompress(hic, 0, &frame_bih, frame, &out_bih, out_capacity)
.with_context(|| format!("ICDecompress (frame {}/{})", i + 1, frames.len()))?;
eprintln!(
"[decode] frame {}/{}: ICDecompress = {} (output {} bytes)",
i + 1,
frames.len(),
rc as i32,
decoded.len()
);
if i == dump_idx {
for spec in &dump_specs {
let dump = capture_dump(&sandbox, spec)?;
if dump.file.is_none()
&& let Some(hex) = &dump.hex
{
eprintln!(
"[decode] memory dump after frame {} [0x{:x}..0x{:x}] ({} of {} bytes mapped):",
i + 1,
dump.addr,
dump.addr.wrapping_add(dump.len as u32),
dump.mapped_bytes,
dump.len
);
eprint!("{}", render_hexdump(dump.addr, hex));
}
}
}
last_decoded = decoded;
}
if !watch_specs.is_empty() {
print_watch_trace(&sandbox, &watch_specs);
}
if let Some(path) = output {
std::fs::write(path, &last_decoded)
.with_context(|| format!("writing output {}", path.display()))?;
eprintln!(
"[decode] wrote {} bytes to {}",
last_decoded.len(),
path.display()
);
} else {
use std::io::Write as _;
std::io::stdout().write_all(&last_decoded)?;
}
let _ = sandbox.ic_decompress_end(hic);
let _ = sandbox.ic_close(hic);
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
fn encode_cmd(
dll_path: &Path,
input: &Path,
width: u32,
height: u32,
fcc_handler: Option<&str>,
input_format: InputFormat,
quality: u32,
keyframe: bool,
output: Option<&Path>,
max_instructions: u64,
) -> anyhow::Result<()> {
let dll_bytes =
std::fs::read(dll_path).with_context(|| format!("reading {}", dll_path.display()))?;
let dll_name = dll_path
.file_name()
.map_or_else(|| "codec.dll".into(), |n| n.to_string_lossy().into_owned());
let frame =
std::fs::read(input).with_context(|| format!("reading input frame {}", input.display()))?;
let expected_frame_bytes = input_format.frame_bytes(width, height) as usize;
if frame.len() < expected_frame_bytes {
anyhow::bail!(
"input frame is {} bytes but {}x{} {:?} expects {} bytes",
frame.len(),
width,
height,
input_format,
expected_frame_bytes
);
}
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.instruction_budget = Some(max_instructions);
let img = sandbox
.load(&dll_name, &dll_bytes)
.with_context(|| format!("load {}", dll_path.display()))?;
let _ = sandbox
.call_dll_main(&img, univdreams::emulator::DLL_PROCESS_ATTACH)
.with_context(|| "DllMain")?;
sandbox
.install_codec(&img)
.with_context(|| "install_codec")?;
let fcc = fcc_handler.map_or_else(|| derive_default_fcc(dll_path), str::to_owned);
let fcc_type = u32::from_le_bytes(*b"VIDC");
let fcc_handler_u32 = fourcc_to_u32(&fcc);
let in_bih = univdreams::emulator::Bih {
bi_size: 40,
width: width as i32,
height: height as i32,
planes: 1,
bit_count: input_format.bi_bit_count(),
compression: input_format.bi_compression(),
size_image: input_format.frame_bytes(width, height),
..univdreams::emulator::Bih::default()
};
let hic = sandbox
.ic_open(fcc_type, fcc_handler_u32, ICMODE_COMPRESS)
.context("ICOpen(ICMODE_COMPRESS)")?;
if hic == 0 {
anyhow::bail!("codec refused DRV_OPEN(COMPRESS)");
}
eprintln!("[encode] HIC = {hic}; fcc_handler = {fcc:?}");
let (_, out_bih) = sandbox
.ic_compress_get_format(hic, &in_bih)
.context("ICCompressGetFormat")?;
eprintln!(
"[encode] codec picked output: bit_count={} compression={:?} size_image={}",
out_bih.bit_count, out_bih.compression, out_bih.size_image
);
let q = sandbox
.ic_compress_query(hic, &in_bih, Some(&out_bih))
.context("ICCompressQuery")?;
eprintln!("[encode] ICCompressQuery = {} (0 = ICERR_OK)", q as i32);
if (q as i32) != 0 {
anyhow::bail!("codec rejected the input/output BIH pair");
}
let cap = sandbox
.ic_compress_get_size(hic, &in_bih, &out_bih)
.context("ICCompressGetSize")?;
eprintln!("[encode] ICCompressGetSize = {cap} bytes");
let _ = sandbox.ic_compress_begin(hic, &in_bih, &out_bih);
let flags = if keyframe { ICCOMPRESS_KEYFRAME } else { 0 };
let frame_slice = &frame[..expected_frame_bytes];
let result = sandbox
.ic_compress(
hic,
flags,
&in_bih,
frame_slice,
&out_bih,
cap,
0, 0, 0, quality,
None, None, )
.context("ICCompress")?;
eprintln!(
"[encode] ICCompress = {} (output {} bytes, output_bih.size_image={})",
result.lresult as i32,
result.bytes.len(),
result.output_bih.size_image,
);
if let Some(path) = output {
std::fs::write(path, &result.bytes)
.with_context(|| format!("writing output {}", path.display()))?;
eprintln!(
"[encode] wrote {} bytes to {}",
result.bytes.len(),
path.display()
);
} else {
use std::io::Write as _;
std::io::stdout().write_all(&result.bytes)?;
}
let _ = sandbox.ic_compress_end(hic);
let _ = sandbox.ic_close(hic);
Ok(())
}
fn solana_cmd(
program_id: &str,
rpc_url: &str,
use_cache: bool,
save_elf: Option<&Path>,
out: Option<&Path>,
) -> anyhow::Result<()> {
let elf_bytes = univdreams::cli::solana::fetch_program_elf(program_id, rpc_url, use_cache)
.with_context(|| format!("fetch {program_id} from {rpc_url}"))?;
if let Some(path) = save_elf {
std::fs::write(path, &elf_bytes).with_context(|| format!("write {}", path.display()))?;
}
let elf = univdreams::format::elf::Elf64File::parse(&elf_bytes)
.with_context(|| format!("parse stripped ELF from {program_id}"))?;
let source = univdreams::translate::decompile::decompile_to_text(&elf)
.with_context(|| format!("decompile {program_id}"))?;
if let Some(path) = out {
std::fs::write(path, source).with_context(|| format!("write {}", path.display()))?;
} else {
use std::io::Write as _;
std::io::stdout().write_all(source.as_bytes())?;
}
Ok(())
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
fn analyze(
input: &Path,
max_instructions: u64,
as_json: bool,
fail_soft: bool,
stage_vfs: &[PathBuf],
vfs_deps: bool,
call_export: &[String],
export_arg: &[String],
call: &[String],
dump_mem: &[String],
map_blob: &[String],
heap_mb: u32,
watch: &[String],
) -> anyhow::Result<()> {
let export_args: Vec<u32> = export_arg
.iter()
.map(|s| parse_u32_arg(s).with_context(|| format!("--export-arg {s:?}")))
.collect::<anyhow::Result<_>>()?;
let watch_specs: Vec<(u32, u32)> = watch
.iter()
.map(|s| parse_watch_spec(s).with_context(|| format!("--watch {s:?}")))
.collect::<anyhow::Result<_>>()?;
let call_specs: Vec<CallSpec> = call
.iter()
.map(|s| parse_call_spec(s).with_context(|| format!("--call {s:?}")))
.collect::<anyhow::Result<_>>()?;
let dump_specs: Vec<DumpSpec> = dump_mem
.iter()
.map(|s| parse_dump_spec(s).with_context(|| format!("--dump-mem {s:?}")))
.collect::<anyhow::Result<_>>()?;
let blob_specs: Vec<MapBlobSpec> = map_blob
.iter()
.map(|s| parse_map_blob_spec(s).with_context(|| format!("--map-blob {s:?}")))
.collect::<anyhow::Result<_>>()?;
let bytes = std::fs::read(input).with_context(|| format!("read {}", input.display()))?;
if !univdreams::format::pe::is_pe(&bytes) {
anyhow::bail!(
"ud analyze currently only supports PE32 DLLs; {} is not a PE",
input.display()
);
}
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("input");
let mut sandbox = univdreams::emulator::Sandbox::new_with_heap_mb(heap_mb);
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
for dir in stage_vfs {
let n = stage_dir_into_vfs(sandbox.context_mut(), dir)
.with_context(|| format!("stage {}", dir.display()))?;
eprintln!("staged {n} files from {}", dir.display());
}
let load_result = if vfs_deps {
sandbox.load_with_deps(stem, &bytes).map(|(img, _)| img)
} else if fail_soft {
sandbox.load_fail_soft(stem, &bytes).map(|(img, _)| img)
} else {
sandbox.load(stem, &bytes)
};
let image = match load_result {
Ok(img) => img,
Err(e) => {
if as_json {
let pe = univdreams::format::pe::PeFile::parse(&bytes).ok();
let indicators = pe.as_ref().map(extract_indicators).unwrap_or_default();
let report = AnalyzeReport {
input: input.display().to_string(),
image_base: 0,
entry_point: 0,
dll_main: DllMainOutcome::LoadFailed {
message: e.to_string(),
},
export_calls: Vec::new(),
win32_calls: Vec::new(),
win32_calls_by_function: Vec::new(),
coverage: CoverageSummary::default(),
indicators,
instructions_executed: 0,
instruction_budget: max_instructions,
memory_dumps: Vec::new(),
watch_trace: Vec::new(),
debug_log: std::mem::take(&mut sandbox.host.debug_log),
};
let s = serde_json::to_string_pretty(&report)?;
println!("{s}");
return Ok(());
}
anyhow::bail!("load {}: {}", input.display(), e);
}
};
for spec in &blob_specs {
apply_map_blob(&mut sandbox, spec)?;
}
let dll_main_result = sandbox.call_dll_main(&image, univdreams::emulator::DLL_PROCESS_ATTACH);
for &(addr, len) in &watch_specs {
sandbox.mmu.add_watch(addr, len);
}
let mut export_calls = drive_exports(&mut sandbox, &image, call_export, &export_args);
export_calls.extend(drive_call_sequence(&mut sandbox, &image, &call_specs));
let memory_dumps = dump_specs
.iter()
.map(|spec| capture_dump(&sandbox, spec))
.collect::<anyhow::Result<Vec<_>>>()?;
let watch_trace = collect_watch_trace(&sandbox, &watch_specs);
let stub_calls = std::mem::take(&mut sandbox.host.stub_calls);
let _: Vec<String> = std::mem::take(&mut sandbox.host.stub_trace);
let instructions_executed = sandbox.host.instructions_executed;
let win32_calls: Vec<Win32Call> = stub_calls
.into_iter()
.map(|c| Win32Call {
dll: c.dll,
name: c.name,
args: c.args,
return_value: c.ret,
call_site_eip: c.call_site_eip,
})
.collect();
let mut by_func: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for c in &win32_calls {
*by_func.entry(format!("{}!{}", c.dll, c.name)).or_default() += 1;
}
let win32_calls_by_function: Vec<Win32CallCount> = by_func
.into_iter()
.map(|(function, count)| Win32CallCount { function, count })
.collect();
let cov = sandbox.coverage();
let ranges = cov.executed_ranges();
let writes = cov.written_addresses().count();
let smc: Vec<u32> = cov.self_modifying_addresses().collect();
let coverage = CoverageSummary {
executed_addresses: cov.executed_count(),
executed_ranges: ranges.len(),
bytes_written: writes,
self_modifying_bytes: smc.len(),
self_modifying_sample: smc.iter().take(8).copied().collect(),
};
let dll_main = match &dll_main_result {
Ok(ret) => DllMainOutcome::Returned { value: *ret },
Err(e) => DllMainOutcome::Trapped {
message: e.to_string(),
},
};
let pe = univdreams::format::pe::PeFile::parse(&bytes).ok();
let indicators = pe.as_ref().map(extract_indicators).unwrap_or_default();
let report = AnalyzeReport {
input: input.display().to_string(),
image_base: image.image_base,
entry_point: image.entry_point,
dll_main,
export_calls,
win32_calls,
win32_calls_by_function,
coverage,
indicators,
instructions_executed,
instruction_budget: max_instructions,
memory_dumps,
watch_trace,
debug_log: std::mem::take(&mut sandbox.host.debug_log),
};
if as_json {
let s = serde_json::to_string_pretty(&report)?;
println!("{s}");
} else {
report.write_text(input);
}
Ok(())
}
#[allow(
clippy::too_many_lines,
clippy::items_after_statements,
clippy::needless_lifetimes
)]
fn monitor_msi_install(
input: &Path,
bytes: &[u8],
_max_instructions: u64,
as_json: bool,
extra_args: Option<&str>,
dump_vfs: Option<&Path>,
) -> anyhow::Result<()> {
use univdreams::emulator::win32::msiexec;
let cmdline = extra_args.unwrap_or("");
let (_op, properties) = msiexec::parse_msiexec_args(cmdline);
let mut vfs = univdreams::emulator::context::VirtualFs::new();
let mut registry = univdreams::emulator::context::VirtualRegistry::new();
let mut n_files = 0usize;
let mut n_dirs = 0usize;
let mut n_regs = 0usize;
let mut n_bytes = 0u64;
let mut n_real_bytes = 0u64;
let mut debug_log: Vec<String> = Vec::new();
struct ReportingSink<'a> {
vfs: &'a mut univdreams::emulator::context::VirtualFs,
registry: &'a mut univdreams::emulator::context::VirtualRegistry,
n_files: &'a mut usize,
n_dirs: &'a mut usize,
n_regs: &'a mut usize,
n_bytes: &'a mut u64,
n_real_bytes: &'a mut u64,
log: &'a mut Vec<String>,
}
impl msiexec::InstallSink for ReportingSink<'_> {
fn emit(&mut self, action: msiexec::InstallAction) -> bool {
match action {
msiexec::InstallAction::CreateDirectory { path, .. } => {
*self.n_dirs += 1;
let marker = format!("{}\\.dir", path.trim_end_matches(['\\', '/']));
if !self.vfs.contains(&marker) {
self.vfs.insert(&marker, Vec::new());
}
}
msiexec::InstallAction::WriteFile {
path, size, bytes, ..
} => {
*self.n_files += 1;
*self.n_bytes = self.n_bytes.saturating_add(size);
let payload = bytes.unwrap_or_default();
if !payload.is_empty() {
*self.n_real_bytes = self.n_real_bytes.saturating_add(payload.len() as u64);
}
self.vfs.write_path(&path, payload);
}
msiexec::InstallAction::RegSet {
hive,
key,
name,
value,
..
} => {
*self.n_regs += 1;
let key_path = format!("{}\\{}", hive.short(), key);
use univdreams::emulator::context::RegistryValue as RV;
let v = match value {
msiexec::RegValue::Empty => RV::Sz(String::new()),
msiexec::RegValue::Sz(s) => RV::Sz(s),
msiexec::RegValue::ExpandSz(s) => RV::ExpandSz(s),
msiexec::RegValue::Dword(d) => RV::Dword(d),
msiexec::RegValue::Binary(b) => RV::Binary(b),
msiexec::RegValue::MultiSz(v) => RV::MultiSz(v),
};
self.registry.set_value(&key_path, &name, v);
}
msiexec::InstallAction::SnapshotProperties(p) => {
self.log
.push(format!("msiexec: property snapshot ({} entries)", p.len()));
}
msiexec::InstallAction::Log(s) => self.log.push(format!("msiexec: {s}")),
msiexec::InstallAction::CustomAction {
name,
action_type,
source,
target,
..
} => {
self.log.push(format!(
"msiexec: queued CA {name:?} type={action_type:#x} src={source:?} tgt={target:?}"
));
}
msiexec::InstallAction::BinaryStream { name, bytes } => {
self.log.push(format!(
"msiexec: captured Binary {name:?} ({} bytes)",
bytes.len()
));
}
}
true
}
}
let mut sink = ReportingSink {
vfs: &mut vfs,
registry: &mut registry,
n_files: &mut n_files,
n_dirs: &mut n_dirs,
n_regs: &mut n_regs,
n_bytes: &mut n_bytes,
n_real_bytes: &mut n_real_bytes,
log: &mut debug_log,
};
let options = msiexec::InstallOptions {
properties,
..Default::default()
};
let result = msiexec::process_msi(bytes, &options, &mut sink);
let outcome_label = match &result {
Ok(_) => format!(
"msi walk OK — {n_files} files ({n_real_bytes}/{n_bytes} bytes extracted), \
{n_dirs} directories, {n_regs} registry entries"
),
Err(e) => format!("msi walk failed: {e}"),
};
debug_log.push(outcome_label.clone());
if let Ok(props) = &result {
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(2_000_000_000);
let mut sb_vfs = univdreams::emulator::context::VirtualFs::new();
for (p, _) in vfs.list() {
if let Some(b) = vfs.read(p) {
sb_vfs.insert(p, b.to_vec());
}
}
sb_vfs.insert("c:/temp/install.msi", bytes.to_vec());
sandbox.context_mut().vfs =
Some(univdreams::emulator::fsmount::MountTable::with_root(sb_vfs));
let mut sb_reg = univdreams::emulator::context::VirtualRegistry::new();
for (k, n, v) in registry.all_values() {
sb_reg.set_value(k, n, v.clone());
}
sandbox.context_mut().registry = Some(sb_reg);
let synth_cmd = format!(
"msiexec.exe /i \"c:/temp/install.msi\" {}",
extra_args.unwrap_or("")
);
univdreams::emulator::win32::msiexec::dispatch_msiexec_install(
&mut sandbox.host,
&mut sandbox.mmu,
"c:/temp/install.msi",
&synth_cmd,
);
let pumped = sandbox.pump_pending_msi_install();
if pumped > 0 {
debug_log.push(format!(
"msiexec: pumped {pumped} deferred CustomAction(s) post-walk"
));
}
if let Some(sb_vfs) = sandbox.context().vfs.as_ref() {
for (p, _) in sb_vfs.list() {
if !vfs.contains(p)
&& let Some(b) = sb_vfs.read(p)
{
vfs.insert(p, b.to_vec());
}
}
}
if let Some(sb_reg) = sandbox.context().registry.as_ref() {
for (k, n, v) in sb_reg.all_values() {
registry.set_value(k, n, v.clone());
}
}
for line in &sandbox.host.debug_log {
debug_log.push(line.clone());
}
let _ = props;
}
let vfs_writes: Vec<VfsEntry> = vfs
.list()
.map(|(p, l)| VfsEntry {
path: p.to_string(),
bytes: l,
})
.collect();
let registry_writes: Vec<RegistryEntry> = registry
.all_values()
.map(|(k, n, v)| RegistryEntry {
key: k.to_string(),
name: n.to_string(),
value: format!("{v:?}"),
})
.collect();
if let Some(dump_root) = dump_vfs {
std::fs::create_dir_all(dump_root)
.with_context(|| format!("create --dump-vfs root {}", dump_root.display()))?;
for (vpath, _) in vfs.list() {
if vpath.ends_with("/.dir") {
continue;
}
let safe = sanitise_vfs_path(vpath);
let out = dump_root.join(safe);
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Some(data) = vfs.read(vpath) {
std::fs::write(&out, data)
.with_context(|| format!("write VFS dump file {}", out.display()))?;
}
}
}
let outcome = match result {
Ok(_) => EntryOutcome::Returned { value: 0 },
Err(e) => EntryOutcome::Trapped {
message: e.to_string(),
},
};
let report = MonitorReport {
input: input.display().to_string(),
image_base: 0,
entry_point: 0,
outcome,
instructions_executed: 0,
instruction_budget: 0,
unresolved_imports: Vec::new(),
win32_calls: Vec::new(),
win32_calls_by_function: Vec::new(),
vfs_writes,
registry_writes,
debug_log,
};
if as_json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
report.write_text();
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn monitor_install_ne(
input: &Path,
bytes: &[u8],
max_instructions: u64,
as_json: bool,
dump_vfs: Option<&Path>,
preserve_deletes: bool,
) -> anyhow::Result<()> {
let stem = input
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("input.exe");
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox.host.preserve_deletes = preserve_deletes;
sandbox
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
sandbox
.context_mut()
.registry
.get_or_insert_with(univdreams::emulator::context::VirtualRegistry::new);
let image = sandbox
.load_ne_fail_soft(stem, bytes)
.with_context(|| format!("NE load {}", input.display()))?;
let entry_base = image
.selectors
.iter()
.find(|(sel, _)| *sel == image.entry_cs)
.map_or(univdreams::emulator::ne::WIN16_SEG_BASE, |(_, base)| *base);
let entry_linear = entry_base.wrapping_add(u32::from(image.entry_ip));
let entry_result = sandbox.call_ne_entry(&image);
let instructions_executed = sandbox.cpu.instr_count;
let stub_calls = std::mem::take(&mut sandbox.host.stub_calls);
let outcome = match &entry_result {
Ok(ret) => EntryOutcome::Returned { value: *ret },
Err(e) => EntryOutcome::Trapped {
message: e.to_string(),
},
};
let win32_calls: Vec<Win32Call> = stub_calls
.into_iter()
.map(|c| Win32Call {
dll: c.dll,
name: c.name,
args: c.args,
return_value: c.ret,
call_site_eip: c.call_site_eip,
})
.collect();
let mut by_func: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for c in &win32_calls {
*by_func.entry(format!("{}!{}", c.dll, c.name)).or_default() += 1;
}
let win32_calls_by_function: Vec<Win32CallCount> = by_func
.into_iter()
.map(|(function, count)| Win32CallCount { function, count })
.collect();
let ctx = sandbox.context();
let vfs_writes: Vec<VfsEntry> = ctx
.vfs
.as_ref()
.map(|vfs| {
vfs.list()
.map(|(path, len)| VfsEntry {
path: path.to_string(),
bytes: len,
})
.collect()
})
.unwrap_or_default();
if let Some(dump_root) = dump_vfs
&& let Some(vfs) = ctx.vfs.as_ref()
{
std::fs::create_dir_all(dump_root)
.with_context(|| format!("create --dump-vfs root {}", dump_root.display()))?;
for (vpath, _) in vfs.list() {
if vpath.ends_with("/.dir") {
continue;
}
let out = dump_root.join(sanitise_vfs_path(vpath));
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Some(data) = vfs.read(vpath) {
std::fs::write(&out, data)
.with_context(|| format!("write VFS dump file {}", out.display()))?;
}
}
}
let registry_writes: Vec<RegistryEntry> = ctx
.registry
.as_ref()
.map(|reg| {
reg.all_values()
.map(|(key, name, value)| RegistryEntry {
key: key.to_string(),
name: name.to_string(),
value: format!("{value:?}"),
})
.collect()
})
.unwrap_or_default();
let unresolved_imports: Vec<UnresolvedImport> = image
.unresolved
.iter()
.cloned()
.map(|(dll, name)| UnresolvedImport { dll, name })
.collect();
let debug_log = std::mem::take(&mut sandbox.host.debug_log);
let report = MonitorReport {
input: input.display().to_string(),
image_base: univdreams::emulator::ne::WIN16_SEG_BASE,
entry_point: entry_linear,
outcome,
instructions_executed,
instruction_budget: max_instructions,
unresolved_imports,
win32_calls,
win32_calls_by_function,
vfs_writes,
registry_writes,
debug_log,
};
let gui = std::mem::take(&mut sandbox.host.gui);
if as_json {
let combined = serde_json::json!({ "report": report, "gui": gui });
println!("{}", serde_json::to_string_pretty(&combined)?);
} else {
report.write_text();
print_gui_transcript(&gui);
}
Ok(())
}
#[cfg(feature = "fstool")]
fn resolve_fs_type(ty: &str, default_type: &str) -> String {
if ty.is_empty() || ty == "auto" || ty == "default" {
default_type.to_string()
} else {
ty.to_string()
}
}
#[cfg(feature = "fstool")]
fn parse_mount_spec(
src: &str,
default_type: &str,
) -> anyhow::Result<univdreams::emulator::fsmount::fstool::FsToolMount> {
use univdreams::emulator::fsmount::fstool::FsToolMount;
if let Some((lhs, rhs)) = src.split_once('@') {
let ty = resolve_fs_type(lhs, default_type);
let (path, writeback) = rhs.strip_suffix(":rw").map_or((rhs, false), |p| (p, true));
Ok(FsToolMount::open_image(
std::path::Path::new(path),
&ty,
writeback,
)?)
} else {
Ok(FsToolMount::format_empty(src, 256 << 20)?)
}
}
#[cfg(feature = "fstool")]
const DEFAULT_ALPINE_VERSION: &str = "3.21.0";
#[cfg(feature = "fstool")]
const ROOTFS_HEADROOM: u64 = 512 << 20;
#[cfg(feature = "fstool")]
fn ud_cache_dir() -> std::path::PathBuf {
let base = std::env::var_os("XDG_CACHE_HOME")
.map(std::path::PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".cache")))
.unwrap_or_else(std::env::temp_dir);
base.join("univdreams").join("rootfs")
}
#[cfg(feature = "fstool")]
fn fetch_cached(url: &str, dest: &std::path::Path) -> anyhow::Result<()> {
if dest.is_file() {
return Ok(());
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
eprintln!("ud: fetching {url}");
let resp = ureq::get(url)
.call()
.with_context(|| format!("GET {url}"))?;
let tmp = dest.with_extension("part");
{
let mut out = std::fs::File::create(&tmp)?;
std::io::copy(&mut resp.into_reader(), &mut out)?;
}
std::fs::rename(&tmp, dest)?;
Ok(())
}
#[cfg(feature = "fstool")]
fn resolve_rootfs_source(spec: &str) -> anyhow::Result<std::path::PathBuf> {
if spec == "alpine" || spec.starts_with("alpine:") || spec.starts_with("alpine/") {
let rest = &spec["alpine".len()..];
let (ver_part, arch) = match rest.split_once('/') {
Some((v, a)) => (v, a.to_string()),
None => (rest, "x86_64".to_string()),
};
let version = ver_part
.strip_prefix(':')
.filter(|v| !v.is_empty())
.unwrap_or(DEFAULT_ALPINE_VERSION);
let branch = {
let mut it = version.split('.');
match (it.next(), it.next()) {
(Some(maj), Some(min)) => format!("v{maj}.{min}"),
_ => anyhow::bail!("invalid alpine version {version:?} (want MAJ.MIN.PATCH)"),
}
};
let file = format!("alpine-minirootfs-{version}-{arch}.tar.gz");
let url = format!("https://dl-cdn.alpinelinux.org/alpine/{branch}/releases/{arch}/{file}");
let dest = ud_cache_dir().join(&file);
fetch_cached(&url, &dest)?;
return Ok(dest);
}
let path = std::path::PathBuf::from(spec);
anyhow::ensure!(path.exists(), "--rootfs source not found: {spec}");
Ok(path)
}
#[cfg(feature = "fstool")]
fn rootfs_image_path(source: &std::path::Path) -> anyhow::Result<std::path::PathBuf> {
use std::hash::{Hash, Hasher};
let canon = std::fs::canonicalize(source).unwrap_or_else(|_| source.to_path_buf());
let meta = std::fs::metadata(&canon)?;
let mut h = std::collections::hash_map::DefaultHasher::new();
canon.hash(&mut h);
meta.len().hash(&mut h);
if let Ok(mtime) = meta.modified()
&& let Ok(dur) = mtime.duration_since(std::time::UNIX_EPOCH)
{
dur.as_secs().hash(&mut h);
}
ROOTFS_HEADROOM.hash(&mut h);
let stem = canon
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("rootfs");
Ok(ud_cache_dir().join(format!("{stem}-{:016x}.ext4", h.finish())))
}
#[cfg(feature = "fstool")]
fn alpine_branch_for(spec: &str) -> Option<String> {
if spec != "alpine" && !spec.starts_with("alpine:") && !spec.starts_with("alpine/") {
return None;
}
let rest = &spec["alpine".len()..];
let ver_part = rest.split_once('/').map_or(rest, |(v, _)| v);
let version = ver_part
.strip_prefix(':')
.filter(|v| !v.is_empty())
.unwrap_or(DEFAULT_ALPINE_VERSION);
let mut it = version.split('.');
match (it.next(), it.next()) {
(Some(maj), Some(min)) => Some(format!("v{maj}.{min}")),
_ => None,
}
}
#[cfg(feature = "fstool")]
fn install_rootfs(
table: &mut univdreams::emulator::fsmount::MountTable,
spec: &str,
) -> anyhow::Result<()> {
use univdreams::emulator::context::FileAccess;
use univdreams::emulator::fsmount::fstool::{FsToolMount, build_ext_image};
let source = resolve_rootfs_source(spec)?;
let image = rootfs_image_path(&source)?;
if !image.is_file() {
if let Some(parent) = image.parent() {
std::fs::create_dir_all(parent)?;
}
eprintln!(
"ud: building rootfs image {} from {}",
image.display(),
source.display()
);
build_ext_image(&image, "ext4", &source, ROOTFS_HEADROOM)
.with_context(|| format!("repack {} -> ext4", source.display()))?;
}
let fm = FsToolMount::open_image(&image, "ext4", true)
.with_context(|| format!("mount rootfs image {}", image.display()))?;
table.mount("/", Box::new(fm));
let resolv = b"nameserver 1.1.1.1\nnameserver 8.8.8.8\n";
if let Some(h) = table.open("/etc/resolv.conf", FileAccess::ReadWrite) {
table.write_handle(h, resolv);
table.close(h);
}
if let Some(branch) = alpine_branch_for(spec) {
let repos = format!(
"http://dl-cdn.alpinelinux.org/alpine/{branch}/main\n\
http://dl-cdn.alpinelinux.org/alpine/{branch}/community\n"
);
let _ = table.truncate_path("/etc/apk/repositories", 0);
if let Some(h) = table.open("/etc/apk/repositories", FileAccess::ReadWrite) {
table.write_handle(h, repos.as_bytes());
table.close(h);
}
}
Ok(())
}
#[cfg(feature = "fstool")]
fn install_fstool_mounts(
table: &mut univdreams::emulator::fsmount::MountTable,
fs: &FsOpts,
default_type: &str,
) -> anyhow::Result<()> {
use univdreams::emulator::fsmount::fstool::FsToolMount;
if let Some(spec) = &fs.rootfs {
install_rootfs(table, spec)?;
} else if let Some(ty) = &fs.fs_type {
let ty = resolve_fs_type(ty, default_type);
let fm = FsToolMount::format_empty(&ty, 256 << 20)
.with_context(|| format!("mkfs root --fs-type {ty}"))?;
table.mount("/", Box::new(fm));
}
for spec in &fs.mounts {
let (point, src) = spec
.split_once('=')
.with_context(|| format!("--mount expects POINT=SPEC, got {spec:?}"))?;
let fm =
parse_mount_spec(src, default_type).with_context(|| format!("--mount {spec:?}"))?;
table.mount(point, Box::new(fm));
}
Ok(())
}
#[cfg(not(feature = "fstool"))]
fn install_fstool_mounts(
_table: &mut univdreams::emulator::fsmount::MountTable,
fs: &FsOpts,
_default_type: &str,
) -> anyhow::Result<()> {
if fs.fs_type.is_some() || !fs.mounts.is_empty() || fs.rootfs.is_some() {
anyhow::bail!("--fs-type / --mount / --rootfs require a build with `--features fstool`");
}
Ok(())
}
fn shell_split(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_tok = false;
let mut quote: Option<char> = None;
for c in s.chars() {
match quote {
Some(q) => {
if c == q {
quote = None;
} else {
cur.push(c);
}
}
None => match c {
'\'' | '"' => {
quote = Some(c);
in_tok = true;
}
c if c.is_whitespace() => {
if in_tok {
out.push(std::mem::take(&mut cur));
in_tok = false;
}
}
c => {
cur.push(c);
in_tok = true;
}
},
}
}
if in_tok {
out.push(cur);
}
out
}
fn default_linux_env() -> Vec<String> {
[
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/root",
"TERM=dumb",
"PWD=/",
"USER=root",
]
.iter()
.map(ToString::to_string)
.collect()
}
#[allow(clippy::too_many_arguments)]
struct RawTerminal {
saved: Option<libc::termios>,
}
impl RawTerminal {
#[allow(unsafe_code)]
fn enable() -> Self {
let fd = libc::STDIN_FILENO;
unsafe {
let mut saved: libc::termios = std::mem::zeroed();
if libc::isatty(fd) != 1 || libc::tcgetattr(fd, &raw mut saved) != 0 {
return Self { saved: None };
}
let mut raw = saved;
libc::cfmakeraw(&raw mut raw);
libc::tcsetattr(fd, libc::TCSANOW, &raw const raw);
Self { saved: Some(saved) }
}
}
}
impl Drop for RawTerminal {
#[allow(unsafe_code)]
fn drop(&mut self) {
if let Some(saved) = self.saved {
unsafe {
libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &raw const saved);
}
}
}
}
#[allow(unsafe_code)]
fn host_term_size() -> (u16, u16) {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) == 0 {
(ws.ws_row, ws.ws_col)
} else {
(0, 0)
}
}
}
#[allow(
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::fn_params_excessive_bools
)]
fn monitor_install_elf(
input: &Path,
bytes: &[u8],
max_instructions: u64,
as_json: bool,
extra_args: Option<&str>,
dump_vfs: Option<&Path>,
use_kvm: bool,
use_net: bool,
use_interactive: bool,
fs: &FsOpts,
) -> anyhow::Result<()> {
let stem = input
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("a.out");
let argv = shell_split(extra_args.unwrap_or(""));
let term_size = if use_interactive {
host_term_size()
} else {
(0, 0)
};
let _raw_guard = if use_interactive {
Some(RawTerminal::enable())
} else {
None
};
if !univdreams::emulator::linux::loader::is_runnable(bytes) {
let machine =
univdreams::format::elf::Elf64File::parse(bytes).map_or(0, |e| e.ehdr.e_machine);
anyhow::bail!(
"ELF executor for e_machine={machine} is not implemented yet \
(i386 / x86-64 / aarch64 run); {} cannot be executed",
input.display()
);
}
let entry =
univdreams::format::elf::Elf64File::parse(bytes).map_or(0, |e| e.ehdr.e_entry as u32);
#[cfg(feature = "kvm")]
let machine = univdreams::format::elf::Elf64File::parse(bytes).map_or(0, |e| e.ehdr.e_machine);
let new_sandbox = || -> anyhow::Result<univdreams::emulator::Sandbox> {
let mut sb = univdreams::emulator::Sandbox::new_linux();
sb.host.instruction_budget = Some(max_instructions);
sb.linux_default_mounts = !fs.no_default_mounts;
sb.linux_argv.clone_from(&argv);
sb.linux_env = default_linux_env();
sb.linux.net_enabled = use_net;
sb.linux.interactive = use_interactive;
sb.linux.term_size = term_size;
let table = sb
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
install_fstool_mounts(table, fs, "ext4")?;
Ok(sb)
};
let mut sandbox = new_sandbox()?;
let run_with_interp =
|sandbox: &mut univdreams::emulator::Sandbox| -> anyhow::Result<Result<i32, _>> {
sandbox
.load_linux_elf(stem, bytes)
.with_context(|| format!("ELF load {}", input.display()))?;
Ok(sandbox.run_linux())
};
#[allow(unused_assignments, unused_mut)]
let mut used_kvm = false;
let run;
#[cfg(feature = "kvm")]
{
const EM_X86_64: u16 = 62;
if use_kvm && machine == EM_X86_64 {
match sandbox.run_linux_kvm(stem, bytes) {
Ok(code) => {
used_kvm = true;
run = Ok(code);
}
Err(e) => {
eprintln!("kvm: {e} — falling back to the interpreter");
sandbox = new_sandbox()?;
run = run_with_interp(&mut sandbox)?;
}
}
} else {
if use_kvm {
eprintln!("note: --kvm accelerates amd64 only; using the interpreter");
}
run = run_with_interp(&mut sandbox)?;
}
}
#[cfg(not(feature = "kvm"))]
{
if use_kvm {
eprintln!("note: this build lacks `--features kvm`; using the interpreter");
}
run = run_with_interp(&mut sandbox)?;
}
let instructions = if used_kvm {
0
} else {
sandbox
.aarch64
.as_ref()
.map_or(sandbox.cpu.instr_count, |c| c.instr_count)
};
if let Some(dump_root) = dump_vfs
&& let Some(vfs) = sandbox.context().vfs.as_ref()
{
std::fs::create_dir_all(dump_root)
.with_context(|| format!("create --dump-vfs root {}", dump_root.display()))?;
for (vpath, _) in vfs.list() {
if vpath.ends_with("/.dir") {
continue;
}
let out = dump_root.join(sanitise_vfs_path(vpath));
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Some(data) = vfs.read(vpath) {
std::fs::write(&out, data).ok();
}
}
}
let vfs_writes: Vec<(String, usize)> = sandbox
.context()
.vfs
.as_ref()
.map(|v| {
v.list()
.filter(|(p, _)| !p.ends_with("/.dir"))
.map(|(p, n)| (p.to_string(), n))
.collect()
})
.unwrap_or_default();
let k = &sandbox.linux;
let stdout = String::from_utf8_lossy(&k.stdout).into_owned();
let stderr = String::from_utf8_lossy(&k.stderr).into_owned();
let exit = match &run {
Ok(code) => Some(*code),
Err(_) => None,
};
let trap = run.as_ref().err().map(ToString::to_string);
let unsupported: Vec<(String, u64)> =
k.unsupported.iter().map(|(s, n)| (s.clone(), *n)).collect();
if use_interactive {
} else if as_json {
let v = serde_json::json!({
"input": input.display().to_string(),
"personality": "linux-i386",
"entry": format!("{entry:#010x}"),
"instructions_executed": instructions,
"instruction_budget": max_instructions,
"exit_code": exit,
"trap": trap,
"stdout": stdout,
"stderr": stderr,
"unsupported_syscalls": unsupported.iter()
.map(|(s, n)| serde_json::json!({"syscall": s, "count": n}))
.collect::<Vec<_>>(),
"vfs_writes": vfs_writes.iter()
.map(|(p, n)| serde_json::json!({"path": p, "bytes": n}))
.collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&v)?);
} else {
println!("ud analyze --monitor (Linux/i386): {}", input.display());
println!(" entry: {entry:#010x}");
println!(" instructions executed: {instructions} (budget {max_instructions})");
match (exit, &trap) {
(Some(c), _) => println!(" exit code: {c}"),
(None, Some(t)) => println!(" trapped: {t}"),
(None, None) => println!(" did not exit"),
}
if !unsupported.is_empty() {
println!(" unsupported syscalls:");
for (s, n) in &unsupported {
println!(" {s} ×{n}");
}
}
if !vfs_writes.is_empty() {
println!(" VFS files ({}):", vfs_writes.len());
for (p, n) in &vfs_writes {
println!(" {p} ({n} bytes)");
}
}
if !stdout.is_empty() {
println!(" ── stdout ──\n{stdout}");
}
if !stderr.is_empty() {
println!(" ── stderr ──\n{stderr}");
}
}
Ok(())
}
fn print_gui_transcript(gui: &univdreams::emulator::win16::gui::GuiState) {
use univdreams::emulator::win16::gui::GuiEvent;
if gui.events.is_empty() {
return;
}
println!(" ── GUI transcript ── ({} events)", gui.events.len());
for ev in &gui.events {
match ev {
GuiEvent::RegisterClass { name } => println!(" RegisterClass {name}"),
GuiEvent::CreateWindow {
hwnd, class, title, ..
} => println!(" CreateWindow hwnd={hwnd:#06x} [{class}] {title:?}"),
GuiEvent::ShowWindow { hwnd, cmd } => {
println!(" ShowWindow hwnd={hwnd:#06x} cmd={cmd}");
}
GuiEvent::MessageBox {
caption,
text,
result,
..
} => println!(" MessageBox {caption:?}: {text:?} -> {result}"),
GuiEvent::DialogStart { title, controls } => {
println!(" Dialog {title:?} ({} controls):", controls.len());
for c in controls {
println!(" [{}] {:?} id={}", c.class, c.text, c.id);
}
}
GuiEvent::DialogEnd { result } => println!(" DialogEnd -> {result}"),
GuiEvent::SetWindowText { hwnd, text } => {
println!(" SetWindowText hwnd={hwnd:#06x} {text:?}");
}
}
}
}
struct FsOpts {
fs_type: Option<String>,
mounts: Vec<String>,
no_default_mounts: bool,
rootfs: Option<String>,
}
#[allow(
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::fn_params_excessive_bools
)]
fn monitor_install(
input: &Path,
max_instructions: u64,
as_json: bool,
extra_args: Option<&str>,
dump_vfs: Option<&Path>,
preserve_deletes: bool,
use_kvm: bool,
use_net: bool,
use_interactive: bool,
fs: &FsOpts,
) -> anyhow::Result<()> {
let bytes = std::fs::read(input).with_context(|| format!("read {}", input.display()))?;
let msi_magic = bytes
.get(..8)
.is_some_and(|h| h == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]);
if msi_magic {
return monitor_msi_install(
input,
&bytes,
max_instructions,
as_json,
extra_args,
dump_vfs,
);
}
if univdreams::format::elf::is_elf(&bytes) {
return monitor_install_elf(
input,
&bytes,
max_instructions,
as_json,
extra_args,
dump_vfs,
use_kvm,
use_net,
use_interactive,
fs,
);
}
if univdreams::format::ne::is_ne(&bytes) {
return monitor_install_ne(
input,
&bytes,
max_instructions,
as_json,
dump_vfs,
preserve_deletes,
);
}
if !univdreams::format::pe::is_pe(&bytes) {
anyhow::bail!(
"ud analyze --monitor requires a PE32, NE, ELF, or MSI binary; {} is neither",
input.display()
);
}
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("input");
let mut sandbox = univdreams::emulator::Sandbox::new();
sandbox.host.trace_stubs = true;
sandbox.host.instruction_budget = Some(max_instructions);
sandbox.host.preserve_deletes = preserve_deletes;
sandbox
.context_mut()
.vfs
.get_or_insert_with(univdreams::emulator::fsmount::MountTable::new);
sandbox
.context_mut()
.registry
.get_or_insert_with(univdreams::emulator::context::VirtualRegistry::new);
let (image, unresolved) = sandbox
.load_fail_soft(stem, &bytes)
.with_context(|| format!("fail-soft load {}", input.display()))?;
let exe_name = input
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("setup.exe");
let command_line = match extra_args {
Some(args) if !args.is_empty() => format!("\"{exe_name}\" {args}"),
_ => format!("\"{exe_name}\""),
};
sandbox.set_command_line(&command_line)?;
let entry_result = sandbox.call_entry_point(&image);
let pumped = sandbox.pump_pending_msi_install();
if pumped > 0 {
sandbox
.host
.debug_log
.push(format!("msiexec: pumped {pumped} deferred CustomAction(s)"));
}
let stub_calls = std::mem::take(&mut sandbox.host.stub_calls);
let instructions_executed = sandbox.host.instructions_executed;
let outcome = match &entry_result {
Ok(ret) => EntryOutcome::Returned { value: *ret },
Err(e) => EntryOutcome::Trapped {
message: e.to_string(),
},
};
let win32_calls: Vec<Win32Call> = stub_calls
.into_iter()
.map(|c| Win32Call {
dll: c.dll,
name: c.name,
args: c.args,
return_value: c.ret,
call_site_eip: c.call_site_eip,
})
.collect();
let mut by_func: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for c in &win32_calls {
*by_func.entry(format!("{}!{}", c.dll, c.name)).or_default() += 1;
}
let win32_calls_by_function: Vec<Win32CallCount> = by_func
.into_iter()
.map(|(function, count)| Win32CallCount { function, count })
.collect();
let ctx = sandbox.context();
let vfs_writes: Vec<VfsEntry> = ctx
.vfs
.as_ref()
.map(|vfs| {
vfs.list()
.map(|(path, len)| VfsEntry {
path: path.to_string(),
bytes: len,
})
.collect()
})
.unwrap_or_default();
if let Some(dump_root) = dump_vfs
&& let Some(vfs) = ctx.vfs.as_ref()
{
std::fs::create_dir_all(dump_root)
.with_context(|| format!("create --dump-vfs root {}", dump_root.display()))?;
for (vpath, _) in vfs.list() {
if vpath.ends_with("/.dir") {
continue;
}
let safe = sanitise_vfs_path(vpath);
let out = dump_root.join(safe);
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Some(data) = vfs.read(vpath) {
std::fs::write(&out, data)
.with_context(|| format!("write VFS dump file {}", out.display()))?;
}
}
}
let registry_writes: Vec<RegistryEntry> = ctx
.registry
.as_ref()
.map(|reg| {
reg.all_values()
.map(|(key, name, value)| RegistryEntry {
key: key.to_string(),
name: name.to_string(),
value: format!("{value:?}"),
})
.collect()
})
.unwrap_or_default();
let unresolved_imports: Vec<UnresolvedImport> = unresolved
.into_iter()
.map(|(dll, name)| UnresolvedImport { dll, name })
.collect();
let debug_log = std::mem::take(&mut sandbox.host.debug_log);
let report = MonitorReport {
input: input.display().to_string(),
image_base: image.image_base,
entry_point: image.entry_point,
outcome,
instructions_executed,
instruction_budget: max_instructions,
unresolved_imports,
win32_calls,
win32_calls_by_function,
vfs_writes,
registry_writes,
debug_log,
};
if as_json {
let s = serde_json::to_string_pretty(&report)?;
println!("{s}");
} else {
report.write_text();
}
Ok(())
}
#[derive(serde::Serialize)]
struct MonitorReport {
input: String,
image_base: u32,
entry_point: u32,
outcome: EntryOutcome,
instructions_executed: u64,
instruction_budget: u64,
unresolved_imports: Vec<UnresolvedImport>,
win32_calls: Vec<Win32Call>,
win32_calls_by_function: Vec<Win32CallCount>,
vfs_writes: Vec<VfsEntry>,
registry_writes: Vec<RegistryEntry>,
debug_log: Vec<String>,
}
#[derive(serde::Serialize)]
#[serde(tag = "status")]
enum EntryOutcome {
Returned { value: u32 },
Trapped { message: String },
}
#[derive(serde::Serialize)]
struct UnresolvedImport {
dll: String,
name: String,
}
#[derive(serde::Serialize)]
struct VfsEntry {
path: String,
bytes: usize,
}
#[derive(serde::Serialize)]
struct RegistryEntry {
key: String,
name: String,
value: String,
}
fn stage_dir_into_vfs(
ctx: &mut univdreams::emulator::context::Context,
dir: &Path,
) -> anyhow::Result<usize> {
let vfs = ctx
.vfs
.as_mut()
.ok_or_else(|| anyhow::anyhow!("VFS not initialised"))?;
let mut count = 0;
let root = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
let mut stack = vec![root.clone()];
while let Some(cur) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&cur) else {
continue;
};
for entry in rd.flatten() {
let path = entry.path();
let Ok(ft) = entry.file_type() else { continue };
if ft.is_dir() {
stack.push(path);
continue;
}
if !ft.is_file() {
continue;
}
let Ok(rel) = path.strip_prefix(&root) else {
continue;
};
let mut s = String::with_capacity(rel.as_os_str().len() + 1);
let mut first = true;
for comp in rel.components() {
use std::path::Component;
if let Component::Normal(os) = comp {
let part = os.to_string_lossy();
if !first {
s.push('/');
}
if first && part.len() == 2 && part.ends_with('_') {
s.push(part.chars().next().unwrap().to_ascii_lowercase());
s.push(':');
} else {
s.push_str(&part);
}
first = false;
}
}
let key = s.to_ascii_lowercase();
let Ok(data) = std::fs::read(&path) else {
continue;
};
vfs.insert(&key, data);
count += 1;
}
}
Ok(count)
}
fn sanitise_vfs_path(vpath: &str) -> std::path::PathBuf {
let mut out = String::with_capacity(vpath.len());
for ch in vpath.chars() {
match ch {
':' => out.push('_'),
'\\' => out.push('/'),
c if c.is_control() => {}
c => out.push(c),
}
}
let stripped = out.trim_start_matches('/').to_string();
std::path::PathBuf::from(stripped)
}
impl MonitorReport {
fn write_text(&self) {
println!("install-monitor report for {}", self.input);
println!(" image base: {:#010x}", self.image_base);
println!(" entry point: {:#010x}", self.entry_point);
println!(
" instructions executed: {} (budget {})",
self.instructions_executed, self.instruction_budget
);
match &self.outcome {
EntryOutcome::Returned { value } => {
println!(" entry returned: {value:#010x}");
}
EntryOutcome::Trapped { message } => {
println!(" entry trapped: {message}");
}
}
println!(
" unresolved-import fallbacks installed: {}",
self.unresolved_imports.len()
);
if !self.unresolved_imports.is_empty() {
let mut by_dll: std::collections::BTreeMap<&str, Vec<&str>> =
std::collections::BTreeMap::new();
for u in &self.unresolved_imports {
by_dll.entry(u.dll.as_str()).or_default().push(&u.name);
}
for (dll, mut names) in by_dll {
names.sort_unstable();
println!(" {} ({} unstubbed):", dll, names.len());
for n in names.iter().take(10) {
println!(" {n}");
}
if names.len() > 10 {
println!(" … and {} more", names.len() - 10);
}
}
}
println!(" Win32 calls: {}", self.win32_calls.len());
for c in self.win32_calls_by_function.iter().take(20) {
println!(" {} ×{}", c.function, c.count);
}
println!(" VFS writes: {}", self.vfs_writes.len());
for e in self.vfs_writes.iter().take(20) {
println!(" {} ({} bytes)", e.path, e.bytes);
}
println!(" Registry writes: {}", self.registry_writes.len());
for e in self.registry_writes.iter().take(20) {
println!(" {}\\{} = {}", e.key, e.name, e.value);
}
if !self.debug_log.is_empty() {
println!(" Debug log ({} entries):", self.debug_log.len());
for line in self.debug_log.iter().take(20) {
println!(" {line}");
}
if self.debug_log.len() > 20 {
println!(" … and {} more", self.debug_log.len() - 20);
}
}
}
}
#[derive(serde::Serialize)]
struct AnalyzeReport {
input: String,
image_base: u32,
entry_point: u32,
dll_main: DllMainOutcome,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
export_calls: Vec<ExportCall>,
win32_calls: Vec<Win32Call>,
win32_calls_by_function: Vec<Win32CallCount>,
coverage: CoverageSummary,
indicators: Indicators,
instructions_executed: u64,
instruction_budget: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
memory_dumps: Vec<MemoryDump>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
watch_trace: Vec<WatchEventReport>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
debug_log: Vec<String>,
}
#[derive(serde::Serialize)]
struct WatchEventReport {
seq: u64,
addr: u32,
offset: u32,
width: u8,
value: u64,
eip: u32,
}
fn collect_watch_trace(
sandbox: &univdreams::emulator::Sandbox,
watches: &[(u32, u32)],
) -> Vec<WatchEventReport> {
let bases: Vec<u32> = watches.iter().map(|&(a, _)| a).collect();
sandbox
.mmu
.watch_log()
.iter()
.map(|ev| {
let base = bases
.iter()
.filter(|&&b| b <= ev.addr)
.max()
.copied()
.unwrap_or(ev.addr);
WatchEventReport {
seq: ev.seq,
addr: ev.addr,
offset: ev.addr - base,
width: ev.width,
value: ev.value,
eip: ev.eip,
}
})
.collect()
}
#[derive(serde::Serialize)]
struct ExportCall {
name: String,
va: u32,
args: Vec<u32>,
#[serde(flatten)]
outcome: ExportOutcome,
}
#[derive(serde::Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
enum ExportOutcome {
Returned { value: u32 },
Trapped { message: String },
}
#[derive(serde::Serialize)]
struct MemoryDump {
addr: u32,
len: usize,
mapped_bytes: usize,
#[serde(skip_serializing_if = "Option::is_none")]
file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
hex: Option<String>,
}
fn parse_watch_spec(s: &str) -> anyhow::Result<(u32, u32)> {
let (addr, len) = s.split_once(':').with_context(|| "expected ADDR:LEN")?;
Ok((parse_u32_arg(addr)?, parse_u32_arg(len)?))
}
fn print_watch_trace(sandbox: &univdreams::emulator::Sandbox, watches: &[(u32, u32)]) {
let log = sandbox.mmu.watch_log();
let bases: Vec<u32> = watches.iter().map(|&(a, _)| a).collect();
eprintln!("[watch] {} store(s) hit the watched region(s):", log.len());
for ev in log {
let base = bases
.iter()
.filter(|&&b| b <= ev.addr)
.max()
.copied()
.unwrap_or(ev.addr);
eprintln!(
"[watch] #{:<5} +0x{:<6x} [0x{:08x}] = 0x{:0width$x} (w{}) eip=0x{:08x}",
ev.seq,
ev.addr - base,
ev.addr,
ev.value,
ev.width,
ev.eip,
width = (ev.width as usize) * 2,
);
}
let dropped = sandbox.mmu.watch_dropped();
if dropped > 0 {
eprintln!(
"[watch] … {dropped} further store(s) dropped after the {} cap",
log.len()
);
}
}
struct DumpSpec {
addr: u32,
len: usize,
file: Option<PathBuf>,
}
fn parse_u32_arg(s: &str) -> anyhow::Result<u32> {
let s = s.trim();
let v = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u32::from_str_radix(hex, 16)
} else {
s.parse::<u32>()
};
v.with_context(|| format!("not a 32-bit integer: {s:?}"))
}
fn parse_dump_spec(s: &str) -> anyhow::Result<DumpSpec> {
let (region, file) = match s.split_once('=') {
Some((r, p)) => (r, Some(PathBuf::from(p))),
None => (s, None),
};
let (addr, len) = region
.split_once(':')
.with_context(|| "expected ADDR:LEN[=PATH]")?;
Ok(DumpSpec {
addr: parse_u32_arg(addr)?,
len: parse_u32_arg(len)? as usize,
file,
})
}
struct MapBlobSpec {
va: u32,
file: PathBuf,
}
fn parse_map_blob_spec(s: &str) -> anyhow::Result<MapBlobSpec> {
let (va, path) = s.split_once('=').with_context(|| "expected VA=PATH")?;
Ok(MapBlobSpec {
va: parse_u32_arg(va)?,
file: PathBuf::from(path),
})
}
fn apply_map_blob(
sandbox: &mut univdreams::emulator::Sandbox,
spec: &MapBlobSpec,
) -> anyhow::Result<()> {
let bytes =
std::fs::read(&spec.file).with_context(|| format!("read {}", spec.file.display()))?;
for i in 0..bytes.len() {
let addr = spec.va.wrapping_add(i as u32);
if !sandbox.mmu.is_mapped(addr) {
sandbox.mmu.map(
addr,
1,
univdreams::emulator::emulator::Perm::R | univdreams::emulator::emulator::Perm::W,
);
}
}
sandbox
.mmu
.write_initializer(spec.va, &bytes)
.map_err(|t| anyhow::anyhow!("stage blob at {:#010x}: {t:?}", spec.va))?;
eprintln!(
"staged {} bytes at guest {:#010x} from {}",
bytes.len(),
spec.va,
spec.file.display()
);
Ok(())
}
fn glob_match(pattern: &str, name: &str) -> bool {
match pattern.split_once('*') {
Some((pre, post)) => {
name.len() >= pre.len() + post.len() && name.starts_with(pre) && name.ends_with(post)
}
None => pattern == name,
}
}
struct CallSpec {
name: String,
args: Vec<u32>,
}
fn parse_call_spec(s: &str) -> anyhow::Result<CallSpec> {
let (name, arglist) = match s.split_once(':') {
Some((n, a)) => (n, a),
None => (s, ""),
};
if name.is_empty() {
anyhow::bail!("empty export name");
}
let args = if arglist.is_empty() {
Vec::new()
} else {
arglist
.split(',')
.map(parse_u32_arg)
.collect::<anyhow::Result<_>>()?
};
Ok(CallSpec {
name: name.to_owned(),
args,
})
}
fn drive_call_sequence(
sandbox: &mut univdreams::emulator::Sandbox,
image: &univdreams::emulator::pe::Image,
specs: &[CallSpec],
) -> Vec<ExportCall> {
specs
.iter()
.map(|spec| {
let va = image.export(&spec.name).unwrap_or(0);
let outcome = if va == 0 && !image.exports.contains_key(&spec.name) {
ExportOutcome::Trapped {
message: format!("export {:?} not found in {}", spec.name, image.name),
}
} else {
match sandbox.call_export(image, &spec.name, &spec.args) {
Ok(value) => ExportOutcome::Returned { value },
Err(e) => ExportOutcome::Trapped {
message: e.to_string(),
},
}
};
ExportCall {
name: spec.name.clone(),
va,
args: spec.args.clone(),
outcome,
}
})
.collect()
}
fn drive_exports(
sandbox: &mut univdreams::emulator::Sandbox,
image: &univdreams::emulator::pe::Image,
patterns: &[String],
args: &[u32],
) -> Vec<ExportCall> {
let mut names: Vec<String> = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for pat in patterns {
let mut matched = false;
for name in image.exports.keys() {
if glob_match(pat, name) {
matched = true;
if seen.insert(name.clone()) {
names.push(name.clone());
}
}
}
if !matched {
eprintln!(
"warning: --call-export {pat:?} matched no export in {}",
image.name
);
}
}
names
.into_iter()
.map(|name| {
let va = image.export(&name).unwrap_or(0);
let outcome = match sandbox.call_export(image, &name, args) {
Ok(value) => ExportOutcome::Returned { value },
Err(e) => ExportOutcome::Trapped {
message: e.to_string(),
},
};
ExportCall {
name,
va,
args: args.to_vec(),
outcome,
}
})
.collect()
}
fn capture_dump(
sandbox: &univdreams::emulator::Sandbox,
spec: &DumpSpec,
) -> anyhow::Result<MemoryDump> {
let bytes: Vec<Option<u8>> = (0..spec.len)
.map(|i| sandbox.mmu.load8(spec.addr.wrapping_add(i as u32)).ok())
.collect();
let mapped_bytes = bytes.iter().filter(|b| b.is_some()).count();
let (file, hex) = if let Some(path) = &spec.file {
let raw: Vec<u8> = bytes.iter().map(|b| b.unwrap_or(0)).collect();
std::fs::write(path, &raw)
.with_context(|| format!("write memory dump to {}", path.display()))?;
eprintln!(
"dumped {} bytes ({} mapped) of guest memory at {:#010x} to {}",
spec.len,
mapped_bytes,
spec.addr,
path.display()
);
(Some(path.display().to_string()), None)
} else {
let mut h = String::with_capacity(spec.len * 2);
for b in &bytes {
match b {
Some(v) => {
const HEX: &[u8; 16] = b"0123456789abcdef";
h.push(HEX[(v >> 4) as usize] as char);
h.push(HEX[(v & 0xf) as usize] as char);
}
None => h.push_str(".."),
}
}
(None, Some(h))
};
Ok(MemoryDump {
addr: spec.addr,
len: spec.len,
mapped_bytes,
file,
hex,
})
}
#[derive(serde::Serialize, Default)]
struct Indicators {
urls: Vec<String>,
file_paths: Vec<String>,
registry_keys: Vec<String>,
ascii_strings: Vec<String>,
}
const STRING_MIN_LEN: usize = 5;
fn extract_indicators(pe: &univdreams::format::pe::PeFile) -> Indicators {
let mut all: Vec<String> = Vec::new();
for (idx, sh) in pe.sections.iter().enumerate() {
let is_exec = sh.characteristics & 0x2000_0000 != 0;
if is_exec {
continue;
}
let Some(data) = pe.section_data(idx) else {
continue;
};
scan_ascii_strings(data, &mut all);
}
all.sort();
all.dedup();
let urls = all
.iter()
.filter(|s| {
s.starts_with("http://")
|| s.starts_with("https://")
|| s.starts_with("ftp://")
|| s.starts_with("ws://")
|| s.starts_with("wss://")
})
.cloned()
.collect();
let file_paths = all
.iter()
.filter(|s| {
let bytes = s.as_bytes();
(bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/'))
|| s.starts_with("\\\\?\\")
|| s.starts_with("\\\\.\\")
})
.cloned()
.collect();
let registry_keys = all
.iter()
.filter(|s| {
s.starts_with("HKEY_")
|| s.starts_with("HKLM\\")
|| s.starts_with("HKCU\\")
|| s.starts_with("HKCR\\")
|| s.starts_with("HKU\\")
})
.cloned()
.collect();
Indicators {
urls,
file_paths,
registry_keys,
ascii_strings: all,
}
}
fn scan_ascii_strings(buf: &[u8], out: &mut Vec<String>) {
let mut start: Option<usize> = None;
for (i, &b) in buf.iter().enumerate() {
let printable = matches!(b, 0x20..=0x7e);
if printable {
start.get_or_insert(i);
} else if let Some(s) = start.take()
&& i - s >= STRING_MIN_LEN
&& let Ok(text) = std::str::from_utf8(&buf[s..i])
{
out.push(text.to_string());
}
}
if let Some(s) = start {
let end = buf.len();
if end - s >= STRING_MIN_LEN
&& let Ok(text) = std::str::from_utf8(&buf[s..end])
{
out.push(text.to_string());
}
}
}
impl AnalyzeReport {
#[allow(clippy::too_many_lines)]
fn write_text(&self, input: &Path) {
println!(
"loaded: {} (image_base 0x{:x}, entry 0x{:x})",
input.display(),
self.image_base,
self.entry_point
);
println!();
println!("Win32 calls observed: {}", self.win32_calls.len());
for c in &self.win32_calls_by_function {
println!(" {:5}× {}", c.count, c.function);
}
println!();
println!("Coverage:");
println!(
" {} distinct EIP addresses executed",
self.coverage.executed_addresses
);
println!(
" {} executed address ranges (contiguous spans)",
self.coverage.executed_ranges
);
println!(" {} guest bytes written", self.coverage.bytes_written);
println!(
" {} bytes were both written and executed (self-modifying / unpacker)",
self.coverage.self_modifying_bytes
);
if !self.coverage.self_modifying_sample.is_empty() {
let preview: Vec<String> = self
.coverage
.self_modifying_sample
.iter()
.map(|a| format!("0x{a:x}"))
.collect();
println!(" first few: {}", preview.join(", "));
}
println!();
println!(
"Instructions executed: {} of {}",
self.instructions_executed, self.instruction_budget
);
println!();
println!("Indicators:");
println!(
" {} ASCII strings extracted from data sections",
self.indicators.ascii_strings.len()
);
if !self.indicators.urls.is_empty() {
println!(" URLs ({}):", self.indicators.urls.len());
for u in &self.indicators.urls {
println!(" {u}");
}
}
if !self.indicators.file_paths.is_empty() {
println!(" File paths ({}):", self.indicators.file_paths.len());
for p in self.indicators.file_paths.iter().take(8) {
println!(" {p}");
}
if self.indicators.file_paths.len() > 8 {
println!(" … {} more", self.indicators.file_paths.len() - 8);
}
}
if !self.indicators.registry_keys.is_empty() {
println!(" Registry keys ({}):", self.indicators.registry_keys.len());
for k in &self.indicators.registry_keys {
println!(" {k}");
}
}
println!();
match &self.dll_main {
DllMainOutcome::Returned { value } => {
println!("DllMain(DLL_PROCESS_ATTACH) returned 0x{value:x}");
}
DllMainOutcome::Trapped { message } => {
println!("DllMain trapped: {message}");
}
DllMainOutcome::LoadFailed { message } => {
println!("load failed: {message}");
}
}
if !self.export_calls.is_empty() {
println!();
println!("Export calls ({}):", self.export_calls.len());
for c in &self.export_calls {
let args: Vec<String> = c.args.iter().map(|a| format!("0x{a:x}")).collect();
match &c.outcome {
ExportOutcome::Returned { value } => {
println!(
" {}(0x{:x})({}) → 0x{value:x}",
c.name,
c.va,
args.join(", ")
);
}
ExportOutcome::Trapped { message } => {
println!(
" {}(0x{:x})({}) trapped: {message}",
c.name,
c.va,
args.join(", ")
);
}
}
}
}
for d in &self.memory_dumps {
println!();
if let Some(file) = &d.file {
println!(
"Memory dump [0x{:x}..0x{:x}] ({} mapped) → {file}",
d.addr,
d.addr.wrapping_add(d.len as u32),
d.mapped_bytes
);
} else {
println!(
"Memory dump [0x{:x}..0x{:x}] ({} of {} bytes mapped):",
d.addr,
d.addr.wrapping_add(d.len as u32),
d.mapped_bytes,
d.len
);
if let Some(hex) = &d.hex {
print_hexdump(d.addr, hex);
}
}
}
if !self.watch_trace.is_empty() {
println!();
println!(
"Watchpoint write-trace ({} stores, in order):",
self.watch_trace.len()
);
for ev in &self.watch_trace {
println!(
" #{:<5} +0x{:<6x} [0x{:08x}] = 0x{:0width$x} (w{}) eip=0x{:08x}",
ev.seq,
ev.offset,
ev.addr,
ev.value,
ev.width,
ev.eip,
width = (ev.width as usize) * 2,
);
}
}
}
}
fn render_hexdump(base: u32, hex: &str) -> String {
use std::fmt::Write as _;
let cells: Vec<&str> = hex
.as_bytes()
.chunks(2)
.map(|c| std::str::from_utf8(c).unwrap_or(".."))
.collect();
let mut out = String::new();
for (row, chunk) in cells.chunks(16).enumerate() {
let addr = base.wrapping_add((row * 16) as u32);
let mut hexpart = String::new();
let mut asciipart = String::new();
for (i, cell) in chunk.iter().enumerate() {
if i == 8 {
hexpart.push(' ');
}
hexpart.push_str(cell);
hexpart.push(' ');
match u8::from_str_radix(cell, 16) {
Ok(b) if (0x20..=0x7e).contains(&b) => asciipart.push(b as char),
Ok(_) => asciipart.push('.'),
Err(_) => asciipart.push(' '),
}
}
let width = 16 * 3 + 1;
for _ in hexpart.len()..width {
hexpart.push(' ');
}
let _ = writeln!(out, " {addr:08x} {hexpart} |{asciipart}|");
}
out
}
fn print_hexdump(base: u32, hex: &str) {
print!("{}", render_hexdump(base, hex));
}
#[derive(serde::Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
enum DllMainOutcome {
Returned { value: u32 },
Trapped { message: String },
LoadFailed { message: String },
}
#[derive(serde::Serialize)]
struct Win32Call {
dll: String,
name: String,
args: Vec<u32>,
return_value: u32,
call_site_eip: u32,
}
#[derive(serde::Serialize)]
struct Win32CallCount {
function: String,
count: usize,
}
#[derive(serde::Serialize, Default)]
struct CoverageSummary {
executed_addresses: usize,
executed_ranges: usize,
bytes_written: usize,
self_modifying_bytes: usize,
self_modifying_sample: Vec<u32>,
}
fn format_warning(w: &univdreams::translate::compile::AsmWarning) -> String {
match w {
univdreams::translate::compile::AsmWarning::Divergence {
location,
text,
canonical,
} => format!(
"{}: text {:?} disagrees with canonical form {:?}",
format_location(location),
text,
canonical,
),
univdreams::translate::compile::AsmWarning::Undecodable { location, text } => format!(
"{}: pinned bytes don't decode as a valid x86 instruction (text was {:?})",
format_location(location),
text,
),
univdreams::translate::compile::AsmWarning::MultipleInsns {
location,
text,
count,
} => format!(
"{}: pinned bytes decode to {} instructions, not 1 (text was {:?})",
format_location(location),
count,
text,
),
}
}
fn format_location(l: &univdreams::translate::compile::AsmLocation) -> String {
let section = l.section.as_deref().unwrap_or("<top-level>");
let function = l.function.as_deref().unwrap_or("<no fn>");
format!("{section}::{function}#{}", l.stmt_index)
}
fn hex_window(bytes: &[u8]) -> String {
bytes
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ")
}