ud_emulator/runtime.rs
1//! Top-level [`Sandbox`] — owns the MMU, the CPU, the Win32 stub
2//! registry, and the per-emulator host state, and exposes the
3//! "load this DLL and call its DllMain" workflow that the
4//! integration tests + future codec wrapper layers drive.
5//!
6//! This is the highest-level public entry point in the crate.
7//! Round-1 exposed [`Sandbox::load`] + [`Sandbox::call_dll_main`];
8//! round-2 adds the generic [`Sandbox::call_export`] helper that
9//! the `vfw32` host stubs use to invoke the codec's `DriverProc`
10//! synchronously.
11
12use crate::emulator::{mmu::Perm, Cpu, Mmu};
13use crate::pe::{Image, Loader};
14use crate::win32::{
15 call_guest, run_until_sentinel as run_until_sentinel_free, vfw32, HostState, Registry,
16 DATA_IMPORT_BASE,
17};
18
19/// `DllMain` reason code: process is loading the DLL.
20pub const DLL_PROCESS_ATTACH: u32 = 1;
21/// `DllMain` reason code: process is unloading the DLL.
22pub const DLL_PROCESS_DETACH: u32 = 0;
23
24/// Default region the loader can use as the kernel32 heap arena.
25const HEAP_ARENA_START: u32 = 0x6000_0000;
26const HEAP_ARENA_END: u32 = 0x7000_0000;
27
28/// Const-arena region — read-only canned strings handed back from
29/// `GetCommandLineA` / `GetEnvironmentStrings` etc.
30const CONST_ARENA_START: u32 = 0x7000_0000;
31const CONST_ARENA_END: u32 = 0x7010_0000;
32
33/// Data-import slot region — see [`crate::win32::DATA_IMPORT_BASE`].
34/// Holds 4-byte values backing CRT data imports like
35/// `msvcrt!_adjust_fdiv`. 4 KiB is plenty.
36const DATA_IMPORT_REGION_SIZE: u32 = 0x0000_1000;
37
38/// Default guest stack region — plenty of room above the heap.
39const STACK_BOTTOM: u32 = 0x9000_0000;
40const STACK_SIZE: u32 = 0x0010_0000; // 1 MiB
41const STACK_TOP: u32 = STACK_BOTTOM + STACK_SIZE;
42
43/// Thread-stack arena. `CreateThread` carves a 64 KiB region
44/// out of this pool per spawned thread, walking down from the
45/// top. 0x8000_0000 .. 0x9000_0000 = 256 MiB → ~4096 aux
46/// threads, plenty for codec / installer corpora.
47const THREAD_STACK_POOL_BOTTOM: u32 = 0x8000_0000;
48const THREAD_STACK_POOL_SIZE: u32 = 0x1000_0000; // 256 MiB
49const THREAD_STACK_POOL_TOP: u32 = THREAD_STACK_POOL_BOTTOM + THREAD_STACK_POOL_SIZE;
50
51/// Thread Environment Block — Windows places its TEB at
52/// `0x7FFD_E000` historically. We map a 4 KiB page here and
53/// stage the SEH chain head (`FS:[0]`) to `0xFFFF_FFFF` ("end of
54/// chain"). Real Windows fills many more fields; for the codec
55/// CRT init we only need a writable page so the codec's SEH
56/// `__try` setup can save the prior chain head, write its own,
57/// and restore on exit.
58const TEB_BASE: u32 = 0x7FFD_E000;
59const TEB_SIZE: u32 = 0x0000_1000; // 4 KiB
60/// `EXCEPTION_REGISTRATION_RECORD*` initialiser at FS:[0].
61const SEH_END_OF_CHAIN: u32 = 0xFFFF_FFFF;
62
63/// Per-thread TIB pool. `CreateThread` carves a 4 KiB TIB out
64/// of this pool per spawned thread, walking down from the
65/// top. 256 KiB → 64 aux thread TIBs, well above any plausible
66/// codec / installer thread count.
67const TIB_POOL_BOTTOM: u32 = 0x7FFC_0000;
68const TIB_POOL_SIZE: u32 = 0x0001_E000; // ~120 KiB → 30 TIBs
69const TIB_POOL_TOP: u32 = TIB_POOL_BOTTOM + TIB_POOL_SIZE;
70
71/// Child-process pools. `CreateProcessA` loads each spawned PE
72/// at [`CHILD_IMAGE_BASE_START`] + N * `CHILD_IMAGE_STRIDE` and
73/// gives each child a private 16 MiB heap arena out of
74/// `[CHILD_HEAP_POOL_START, CHILD_HEAP_POOL_END)`. Picked above
75/// the parent's mapped regions but below TEB / stack-pool
76/// addresses to keep guest pointers easy to read in traces.
77const CHILD_IMAGE_BASE_START: u32 = 0x1000_0000;
78const CHILD_HEAP_POOL_START: u32 = 0xA000_0000;
79const CHILD_HEAP_POOL_SIZE: u32 = 0x1000_0000; // 256 MiB → 16 children
80const CHILD_HEAP_POOL_END: u32 = CHILD_HEAP_POOL_START + CHILD_HEAP_POOL_SIZE;
81
82/// One sandbox instance per loaded codec DLL.
83pub struct Sandbox {
84 pub mmu: Mmu,
85 pub cpu: Cpu,
86 pub registry: Registry,
87 pub host: HostState,
88}
89
90impl Default for Sandbox {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl Sandbox {
97 /// Borrow the always-on coverage map populated by the
98 /// interpreter. Records every dispatched instruction's
99 /// entry EIP plus every guest memory write. See
100 /// [`crate::coverage::CoverageMap`] for the consumer
101 /// surface.
102 #[must_use]
103 pub fn coverage(&self) -> &crate::coverage::CoverageMap {
104 &self.mmu.coverage
105 }
106
107 /// Mutable accessor for the coverage map — useful for
108 /// per-export resets (`coverage_mut().clear()`) between
109 /// runs of the same sandbox.
110 pub fn coverage_mut(&mut self) -> &mut crate::coverage::CoverageMap {
111 &mut self.mmu.coverage
112 }
113
114 /// Borrow the emulation-context layer (virtual filesystem,
115 /// virtual registry, future surfaces). Always present;
116 /// the per-surface options decide whether the guest
117 /// observes synthetic state or the fail-soft Win32
118 /// default. See [`crate::context::Context`].
119 #[must_use]
120 pub fn context(&self) -> &crate::context::Context {
121 &self.host.context
122 }
123
124 /// Mutable accessor for the context.
125 pub fn context_mut(&mut self) -> &mut crate::context::Context {
126 &mut self.host.context
127 }
128
129 /// Builder: attach a virtual filesystem so guest file-API
130 /// calls land in-memory instead of fail-soft no-ops. See
131 /// [`crate::VirtualFs`] for the stage-some-files / capture-
132 /// what's-written workflow.
133 #[must_use]
134 pub fn with_vfs(mut self, vfs: crate::context::VirtualFs) -> Self {
135 self.host.context.vfs = Some(vfs);
136 self
137 }
138
139 /// Builder: attach a virtual registry so guest `Reg*` calls
140 /// observe analyst-staged keys and writes land in-memory.
141 /// See [`crate::VirtualRegistry`].
142 #[must_use]
143 pub fn with_registry(mut self, reg: crate::context::VirtualRegistry) -> Self {
144 self.host.context.registry = Some(reg);
145 self
146 }
147
148 /// Create a fresh sandbox with the heap arena and stack
149 /// pre-mapped, the kernel32 stub set registered, and the
150 /// CPU's `esp` pointing at a freshly-allocated stack.
151 pub fn new() -> Self {
152 let mut mmu = Mmu::new();
153 // Heap arena (R+W+X). Old codecs (e.g. Cinepak) ship
154 // architecture-specific inner-loop assembly that they
155 // copy into `malloc`'d memory at init time and then call.
156 // On real Windows, `HeapAlloc(GetProcessHeap, ...)` returns
157 // executable memory by default; modelling the same is
158 // simpler than chasing per-codec `VirtualProtect(PAGE_EXEC)`
159 // calls. Bytes still respect `mmu.write_initializer`'s
160 // perm rules; only the X bit is broader.
161 mmu.map(
162 HEAP_ARENA_START,
163 HEAP_ARENA_END - HEAP_ARENA_START,
164 Perm::R | Perm::W | Perm::X,
165 );
166 // Const-arena for canned strings (R+W mapped; the caller
167 // ABI treats it as R-only — we use write_initializer for
168 // population, then any reads honour the perm bits).
169 mmu.map(
170 CONST_ARENA_START,
171 CONST_ARENA_END - CONST_ARENA_START,
172 Perm::R | Perm::W,
173 );
174 // Data-import slot region (R+W) — holds the 4-byte
175 // values backing CRT data imports like
176 // `msvcrt!_adjust_fdiv`. Seeded with each registered
177 // import's `initial` value.
178 mmu.map(DATA_IMPORT_BASE, DATA_IMPORT_REGION_SIZE, Perm::R | Perm::W);
179 // Stack (R+W)
180 mmu.map(STACK_BOTTOM, STACK_SIZE, Perm::R | Perm::W);
181 // Thread-stack pool (R+W). `CreateThread` carves
182 // 64 KiB stacks out of the top of this pool, walking
183 // down with each thread.
184 mmu.map(
185 THREAD_STACK_POOL_BOTTOM,
186 THREAD_STACK_POOL_SIZE,
187 Perm::R | Perm::W,
188 );
189 // Stub-thunk region (R-only, zeroed). The run loop
190 // detects `eip == thunk_addr` via `Registry::is_thunk`
191 // *before* hitting the MMU, so execution still routes
192 // to the stub regardless of the X bit — but codecs that
193 // *read* a function pointer's bytes (a hot-patch /
194 // forwarder probe; CamStudio does this in DllMain) need
195 // a mapped region behind the address. Zeros pass every
196 // standard "is this byte E9/EB/CC/C3?" introspection.
197 mmu.map(crate::win32::THUNK_BASE, 0x1_0000, Perm::R);
198 // TEB / FS-segment data (R+W). Initialise FS:[0] = -1
199 // (no SEH handler installed) and FS:[0x18] = TEB self
200 // pointer per the Windows TEB ABI used by Win32 CRTs.
201 mmu.map(TEB_BASE, TEB_SIZE, Perm::R | Perm::W);
202 mmu.write_initializer(TEB_BASE, &SEH_END_OF_CHAIN.to_le_bytes())
203 .expect("seed TEB FS:[0]");
204 mmu.write_initializer(TEB_BASE + 0x18, &TEB_BASE.to_le_bytes())
205 .expect("seed TEB FS:[0x18] (self pointer)");
206 // Per-thread TIB pool (R+W). `CreateThread` carves a
207 // fresh TIB out of this pool for each spawned thread,
208 // setting the new thread's FS base to its own TIB.
209 mmu.map(TIB_POOL_BOTTOM, TIB_POOL_SIZE, Perm::R | Perm::W);
210 // Child-process heap pool (R+W+X). Each `CreateProcessA`
211 // carves a 16 MiB heap arena from this region for the
212 // spawned child.
213 mmu.map(
214 CHILD_HEAP_POOL_START,
215 CHILD_HEAP_POOL_SIZE,
216 Perm::R | Perm::W | Perm::X,
217 );
218 // FS:[0x30] would be the PEB pointer — we leave it 0
219 // until a codec actually dereferences it.
220
221 let mut cpu = Cpu::new();
222 cpu.regs.set_esp(STACK_TOP - 0x100); // leave a guard at the top
223 cpu.set_fs_base(TEB_BASE);
224
225 let mut registry = Registry::new();
226 registry.register_all();
227 // Seed data-import slot values into the mapped region.
228 for (_dll, _name, d) in registry.data_imports() {
229 mmu.write_initializer(d.addr, &d.initial.to_le_bytes())
230 .expect("seed data import");
231 }
232
233 let mut host = HostState::new(HEAP_ARENA_START, HEAP_ARENA_END)
234 .with_const_arena(CONST_ARENA_START, CONST_ARENA_END)
235 .with_thread_stack_pool(THREAD_STACK_POOL_BOTTOM, THREAD_STACK_POOL_TOP)
236 .with_tib_pool(TIB_POOL_BOTTOM, TIB_POOL_TOP)
237 .with_child_arena(
238 CHILD_IMAGE_BASE_START,
239 CHILD_HEAP_POOL_START,
240 CHILD_HEAP_POOL_END,
241 );
242 // Bootstrap thread's TIB lives at the runtime-owned
243 // TEB_BASE (already mapped + seeded above) — mirror
244 // its address into ThreadState so SetLastError /
245 // GetLastError can also write through `fs:[0x34]`.
246 if let Some(t) = host.threads.get_mut(&1) {
247 t.tib_addr = TEB_BASE;
248 }
249
250 // Pre-register the system DLLs whose stub registries we
251 // ship as "loaded modules". Real Windows always has these
252 // available, and codec CRTs commonly probe them via
253 // `GetModuleHandleW(L"KERNEL32.DLL")` before walking their
254 // exports (e.g. lagarith's `_CRT_INIT` rolls back its heap
255 // and bails if `KERNEL32.DLL`'s handle comes back NULL).
256 // The handles are synthetic, distinct, non-zero values in
257 // the otherwise-unmapped `0x7800_0000..0x7900_0000` band.
258 // Codecs use these handles for identity comparisons and
259 // as opaque arguments to `GetProcAddress`; the band is
260 // clear of every other mapped region (heap, const arena,
261 // TEB, stack, VirtualAlloc range, thunk space) so a
262 // codec that tries to *walk* the handle as if it were a
263 // PE image gets a clean `MemoryFault` rather than
264 // accidentally hitting some other arena.
265 for (i, dll) in [
266 "kernel32.dll",
267 "user32.dll",
268 "gdi32.dll",
269 "advapi32.dll",
270 "ole32.dll",
271 "shell32.dll",
272 "shlwapi.dll",
273 "comctl32.dll",
274 "winmm.dll",
275 "msvcrt.dll",
276 "msvcr71.dll",
277 "msvcr80.dll",
278 "msvcr90.dll",
279 "pncrt.dll",
280 "mfplat.dll",
281 "version.dll",
282 "vfw32.dll",
283 ]
284 .iter()
285 .enumerate()
286 {
287 let handle = 0x7800_0000u32.wrapping_add((i as u32) * 0x10_0000);
288 host.modules.insert((*dll).to_string(), handle);
289 }
290
291 // Round 35 — pre-register the canonical DirectShow memory
292 // allocator class factory in the in-process class-factory
293 // cache. Codecs that internally call
294 // `CoCreateInstance(CLSID_MemoryAllocator, NULL, _,
295 // IID_IMemAllocator, &alloc)` (e.g. mpg4ds32 from inside
296 // `IMemInputPin::GetAllocator`) will now hit our host
297 // factory rather than the round-34 baseline
298 // `CLASS_E_CLASSNOTAVAILABLE` (`0x80040111`) miss. CLSID
299 // value sourced from Windows SDK header `axextend.h`.
300 if let Ok(factory) =
301 crate::com::mint_host_mem_allocator_class_factory(&mut host, &mut mmu, ®istry)
302 {
303 host.com
304 .register_class_factory(crate::com::CLSID_MEMORY_ALLOCATOR, factory);
305 }
306
307 Sandbox {
308 mmu,
309 cpu,
310 registry,
311 host,
312 }
313 }
314
315 /// Builder-style seed setter for the `msvcrt!rand` LCG.
316 ///
317 /// PRNG state for `msvcrt!rand` calls from sandboxed codec
318 /// code. Default `1` matches MSVC's documented "no `srand`
319 /// called yet" initial value. Set via `with_rand_seed` /
320 /// `set_rand_seed` for reproducible encode output: two
321 /// sandboxes seeded identically produce identical `rand`
322 /// sequences, which makes encode regression tests
323 /// deterministic across runs.
324 ///
325 /// The guest's own `msvcrt!srand(seed)` call writes to the
326 /// same field, so the codec may re-seed at any time; in that
327 /// case [`Self::rand_seed`] will report whatever value the
328 /// codec last installed.
329 ///
330 /// Round 55.
331 pub fn with_rand_seed(mut self, seed: u32) -> Self {
332 self.host.rand_state = seed;
333 self
334 }
335
336 /// Set the `msvcrt!rand` LCG state at runtime.
337 ///
338 /// Same contract as [`Self::with_rand_seed`], but mutates an
339 /// already-constructed sandbox — useful for tests that drive
340 /// multiple encode runs with different seeds, or for fuzzing
341 /// harnesses that want to force the codec into a known state
342 /// before each iteration.
343 ///
344 /// Round 55.
345 pub fn set_rand_seed(&mut self, seed: u32) {
346 self.host.rand_state = seed;
347 }
348
349 /// Override the value `kernel32!GetCommandLineA` returns to
350 /// the guest. The string is stashed (NUL-terminated) in the
351 /// host's const arena and a pointer to it is parked at
352 /// `command_line_ptr`. Installer-class binaries consult
353 /// this to pick up `/quiet`, `/qn`, `/S` and similar
354 /// silent-install flags.
355 pub fn set_command_line(&mut self, cmdline: &str) -> Result<(), crate::Error> {
356 let mut bytes = cmdline.as_bytes().to_vec();
357 bytes.push(0);
358 let addr = self
359 .host
360 .arena_const_alloc(bytes.len() as u32)
361 .map_err(crate::Error::Win32)?;
362 self.mmu.write_initializer(addr, &bytes)?;
363 self.host.command_line_ptr = addr;
364 Ok(())
365 }
366
367 /// Read the current `msvcrt!rand` LCG state.
368 ///
369 /// Reflects whatever the host or the guest last wrote: a
370 /// fresh sandbox returns `1` (MSVC's documented "no `srand`
371 /// called yet" initial value); after host
372 /// [`Self::set_rand_seed`] / [`Self::with_rand_seed`] returns
373 /// that value; after a guest `msvcrt!srand(s)` call returns
374 /// `s`; after any number of `msvcrt!rand` calls returns the
375 /// post-step LCG state.
376 ///
377 /// Round 55.
378 pub fn rand_seed(&self) -> u32 {
379 self.host.rand_state
380 }
381
382 /// Load a PE32 image from `bytes`, mapping it into the
383 /// sandbox's MMU. The returned [`Image`] holds the entry
384 /// point + export table.
385 ///
386 /// Strict-resolution: any IAT entry the
387 /// [`crate::win32::Registry`] doesn't satisfy is a hard
388 /// load-time error. Use [`Sandbox::load_fail_soft`] for
389 /// EXEs whose import list exceeds the codec-class stub
390 /// surface (installers, GUI apps, etc.).
391 pub fn load(&mut self, name: &str, bytes: &[u8]) -> Result<Image, crate::Error> {
392 let mut loader = Loader::new(&mut self.mmu, &mut self.registry, &mut self.host);
393 let img = loader.load(name, bytes)?;
394 // Record primary module base so `GetModuleHandleA(NULL)`
395 // returns the right value.
396 self.host.primary_module_base = img.image_base;
397 // Also record the loaded module under its filename so
398 // `GetModuleHandleA("name.dll")` finds it. Lower-cased,
399 // matching the lookup in `stub_get_module_handle_a` /
400 // `_w`.
401 self.host
402 .modules
403 .insert(name.to_ascii_lowercase(), img.image_base);
404 Ok(img)
405 }
406
407 /// Load a PE32 image in fail-soft import-resolution mode.
408 /// Imports the codec-class stub registry doesn't satisfy
409 /// get a trap-on-call fallback thunk so the load succeeds.
410 /// Returns the loaded [`Image`] plus the list of
411 /// `(dll, name)` pairs that received a fallback — i.e.
412 /// the set of APIs the operator now knows the binary uses
413 /// but we don't yet stub.
414 ///
415 /// Intended for the install-monitor workflow: load
416 /// QuickTimeInstaller.exe with fail-soft, drive the entry
417 /// point, watch the trap stream for the next missing API.
418 pub fn load_fail_soft(
419 &mut self,
420 name: &str,
421 bytes: &[u8],
422 ) -> Result<(Image, Vec<(String, String)>), crate::Error> {
423 let mut options = crate::pe::LoadOptions {
424 imports: crate::pe::imports::ResolveMode::FailSoft,
425 fail_soft_log: Some(Vec::new()),
426 target_image_base: None,
427 };
428 let mut loader = Loader::new(&mut self.mmu, &mut self.registry, &mut self.host);
429 let img = loader.load_with_options(name, bytes, &mut options)?;
430 self.host.primary_module_base = img.image_base;
431 self.host
432 .modules
433 .insert(name.to_ascii_lowercase(), img.image_base);
434 Ok((img, options.fail_soft_log.unwrap_or_default()))
435 }
436
437 /// Synchronously call `DllMain(hModule, fdwReason, lpvReserved)`
438 /// inside the emulator and return the dword `eax` value at
439 /// the point the function returned to the synthetic
440 /// `RET_SENTINEL`.
441 ///
442 /// The DllMain ABI is stdcall (callee-cleanup), so we push
443 /// `lpvReserved` first, then `fdwReason`, then `hModule`,
444 /// then the return-address sentinel. The callee's `RET 12`
445 /// (or equivalent) cleans the args.
446 ///
447 /// Resolution: prefer the `DllMain` named export (Indeo
448 /// codecs); fall back to the PE `AddressOfEntryPoint`
449 /// (mpg4c32.dll and other CRT-startup-driven DLLs that
450 /// don't export `DllMain` by name). Both expose the same
451 /// stdcall (HINSTANCE, DWORD, LPVOID) ABI.
452 pub fn call_dll_main(&mut self, image: &Image, reason: u32) -> Result<u32, crate::Error> {
453 let h_module = image.image_base;
454 let lpv_reserved = 0u32;
455 let target = image.export("DllMain").unwrap_or(image.entry_point);
456 if target == 0 {
457 return Err(crate::Error::Win32(
458 crate::win32::Win32Error::InvalidArgument {
459 stub: "call_dll_main",
460 reason: format!(
461 "no DllMain export and no PE entry point in {:?}",
462 image.name
463 ),
464 },
465 ));
466 }
467 call_guest(
468 &mut self.cpu,
469 &mut self.mmu,
470 &self.registry,
471 &mut self.host,
472 target,
473 &[h_module, reason, lpv_reserved],
474 )
475 }
476
477 /// Generic stdcall guest-call helper. Resolves `name` against
478 /// `image`'s export table, pushes `args` right-to-left + the
479 /// `RET_SENTINEL`, and runs until the callee returns.
480 /// Returns `eax`.
481 ///
482 /// Used both internally (by [`Self::call_dll_main`]) and by
483 /// future codec adapter layers that need to drive arbitrary
484 /// codec exports — `DriverProc`, `MyCodecGetVersion`,
485 /// `MyCodecExtraInit`, etc. The round-2 `vfw32::ic_*` host
486 /// surface uses [`crate::win32::call_guest`] directly with
487 /// the codec's `DriverProc` VA.
488 pub fn call_export(
489 &mut self,
490 image: &Image,
491 name: &str,
492 args: &[u32],
493 ) -> Result<u32, crate::Error> {
494 let target = image.export(name).ok_or_else(|| {
495 crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
496 stub: "call_export",
497 reason: format!("export {name:?} not found in {:?}", image.name),
498 })
499 })?;
500 call_guest(
501 &mut self.cpu,
502 &mut self.mmu,
503 &self.registry,
504 &mut self.host,
505 target,
506 args,
507 )
508 }
509
510 /// Call the image's PE entry point (`AddressOfEntryPoint`).
511 /// For an EXE this is the CRT startup, which expects no
512 /// arguments and never returns under normal Windows
513 /// semantics (it calls `ExitProcess`). Here it runs until
514 /// the runtime returns to the synthetic `RET_SENTINEL` or
515 /// hits a trap (e.g. unresolved import, instruction limit).
516 pub fn call_entry_point(&mut self, image: &Image) -> Result<u32, crate::Error> {
517 if image.entry_point == 0 {
518 return Err(crate::Error::Win32(
519 crate::win32::Win32Error::InvalidArgument {
520 stub: "call_entry_point",
521 reason: format!("no PE entry point in {:?}", image.name),
522 },
523 ));
524 }
525 call_guest(
526 &mut self.cpu,
527 &mut self.mmu,
528 &self.registry,
529 &mut self.host,
530 image.entry_point,
531 &[],
532 )
533 }
534
535 /// Drive the CPU until `eip == RET_SENTINEL`, dispatching to
536 /// Win32 stubs whenever `eip` lands on a registered thunk
537 /// address. Thin wrapper over [`crate::win32::run_until_sentinel`]
538 /// kept for API stability.
539 pub fn run_until_sentinel(&mut self) -> Result<(), crate::Error> {
540 run_until_sentinel_free(&mut self.cpu, &mut self.mmu, &self.registry, &mut self.host)
541 }
542
543 // ---- vfw32 IC* convenience wrappers ------------------------------
544
545 /// Mark `image` as the codec the next [`Self::ic_open`] call
546 /// should target.
547 ///
548 /// Round 2 supports a single codec image per sandbox — round 3
549 /// will lift that into a multi-codec registry. The image must
550 /// export `DriverProc`.
551 pub fn install_codec(&mut self, image: &Image) -> Result<(), crate::Error> {
552 let dp = image.export("DriverProc").ok_or_else(|| {
553 crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
554 stub: "install_codec",
555 reason: format!("DriverProc not exported by {:?}", image.name),
556 })
557 })?;
558 self.host.default_driver_proc = dp;
559 Ok(())
560 }
561
562 /// Open the installed codec (`DRV_OPEN`).
563 pub fn ic_open(
564 &mut self,
565 fcc_type: u32,
566 fcc_handler: u32,
567 mode: u32,
568 ) -> Result<u32, crate::Error> {
569 vfw32::ic_open(
570 &mut self.cpu,
571 &mut self.mmu,
572 &self.registry,
573 &mut self.host,
574 fcc_type,
575 fcc_handler,
576 mode,
577 )
578 }
579
580 /// Close a codec instance (`DRV_CLOSE`).
581 pub fn ic_close(&mut self, hic: u32) -> Result<u32, crate::Error> {
582 vfw32::ic_close(
583 &mut self.cpu,
584 &mut self.mmu,
585 &self.registry,
586 &mut self.host,
587 hic,
588 )
589 }
590
591 /// Read the codec's `ICINFO` block.
592 pub fn ic_get_info(&mut self, hic: u32, cb: u32) -> Result<Vec<u8>, crate::Error> {
593 vfw32::ic_get_info(
594 &mut self.cpu,
595 &mut self.mmu,
596 &self.registry,
597 &mut self.host,
598 hic,
599 cb,
600 )
601 }
602
603 /// `ICDecompressQuery` — does the codec accept this format?
604 pub fn ic_decompress_query(
605 &mut self,
606 hic: u32,
607 input: &vfw32::Bih,
608 output: Option<&vfw32::Bih>,
609 ) -> Result<u32, crate::Error> {
610 vfw32::ic_decompress_query(
611 &mut self.cpu,
612 &mut self.mmu,
613 &self.registry,
614 &mut self.host,
615 hic,
616 input,
617 output,
618 )
619 }
620
621 /// `ICDecompressGetFormat` — ask the codec for the output BIH
622 /// matching `input`. Round 30 uses this to probe stream
623 /// dimensions when `CodecParameters` lacks them.
624 pub fn ic_decompress_get_format(
625 &mut self,
626 hic: u32,
627 input: &vfw32::Bih,
628 ) -> Result<(u32, vfw32::Bih), crate::Error> {
629 vfw32::ic_decompress_get_format(
630 &mut self.cpu,
631 &mut self.mmu,
632 &self.registry,
633 &mut self.host,
634 hic,
635 input,
636 )
637 }
638
639 /// `ICDecompressBegin` — set up the decoder pipeline.
640 pub fn ic_decompress_begin(
641 &mut self,
642 hic: u32,
643 input: &vfw32::Bih,
644 output: &vfw32::Bih,
645 ) -> Result<u32, crate::Error> {
646 vfw32::ic_decompress_begin(
647 &mut self.cpu,
648 &mut self.mmu,
649 &self.registry,
650 &mut self.host,
651 hic,
652 input,
653 output,
654 )
655 }
656
657 /// `ICDecompressEnd` — tear down the decoder pipeline.
658 pub fn ic_decompress_end(&mut self, hic: u32) -> Result<u32, crate::Error> {
659 vfw32::ic_decompress_end(
660 &mut self.cpu,
661 &mut self.mmu,
662 &self.registry,
663 &mut self.host,
664 hic,
665 )
666 }
667
668 // ---- Trace-mode programmatic API (gated on the `trace`
669 // ---- Cargo feature). Documented in
670 // ---- `docs/winmf/winmf-emulator.md` §"Trace mode".
671
672 /// Install a memory watchpoint covering `[addr, addr+size)`.
673 /// Any guest access whose address range intersects the
674 /// watchpoint emits a `kind=mem_write` (or `mem_read`) JSONL
675 /// event to the configured sink. Multiple watchpoints may
676 /// overlap; each fires independently.
677 #[cfg(feature = "trace")]
678 pub fn watch(&mut self, addr: u32, size: u32, mode: crate::trace::WatchMode) {
679 self.mmu.trace.watch(addr, size, mode);
680 }
681
682 /// Remove watchpoints whose `(addr, size)` exactly matches.
683 /// Mode is ignored for the match.
684 #[cfg(feature = "trace")]
685 pub fn unwatch(&mut self, addr: u32, size: u32) {
686 self.mmu.trace.unwatch(addr, size);
687 }
688
689 /// Toggle per-instruction execution trace at runtime. Has no
690 /// effect unless the crate was built with the `trace-exec`
691 /// sub-feature.
692 #[cfg(feature = "trace")]
693 pub fn set_exec_trace(&mut self, on: bool) {
694 self.mmu.trace.exec_on = on;
695 }
696
697 /// Override the trace JSONL sink at runtime. Defaults to
698 /// honouring `OXIDEAV_VFW_TRACE_FILE`.
699 #[cfg(feature = "trace")]
700 pub fn set_trace_sink(&mut self, sink: Box<dyn std::io::Write + Send>) {
701 self.mmu.trace.set_sink(sink);
702 }
703
704 // ---- COM / DirectShow surface (round 25) ------------------------
705
706 /// Drive `DllGetClassObject(rclsid, riid, ppv)` on `image`,
707 /// staging the GUID arguments + the `ppv` out-slot in a
708 /// freshly-allocated heap region inside the sandbox. On
709 /// success returns the guest pointer the codec wrote into
710 /// `*ppv` — typically a guest-side `IClassFactory`.
711 ///
712 /// When `riid == IID_IClassFactory`, the returned pointer is
713 /// also registered with [`crate::com::ComObjectTable::register_class_factory`]
714 /// keyed under `clsid`, so subsequent
715 /// [`Self::co_create_instance`] calls can resolve `clsid`
716 /// without re-driving `DllGetClassObject`.
717 ///
718 /// MSDN: `HRESULT DllGetClassObject(REFCLSID rclsid, REFIID
719 /// riid, LPVOID *ppv)` — every COM in-process server
720 /// exports it; DirectShow filter binaries (`.ax`) export it
721 /// instead of `DriverProc`.
722 pub fn dll_get_class_object(
723 &mut self,
724 image: &crate::pe::Image,
725 clsid: crate::com::Guid,
726 riid: crate::com::Guid,
727 ) -> Result<u32, crate::Error> {
728 let target = image.export("DllGetClassObject").ok_or_else(|| {
729 crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
730 stub: "dll_get_class_object",
731 reason: format!("DllGetClassObject not exported by {:?}", image.name),
732 })
733 })?;
734 // Stage the two GUIDs + the out-pointer slot in
735 // contiguous arena memory: 16 + 16 + 4 = 36 bytes.
736 let scratch = self.host.arena_alloc(36).map_err(crate::Error::Win32)?;
737 clsid
738 .stage(&mut self.mmu, scratch)
739 .map_err(crate::Error::Trap)?;
740 riid.stage(&mut self.mmu, scratch + 16)
741 .map_err(crate::Error::Trap)?;
742 // Zero the ppv slot.
743 self.mmu
744 .write_initializer(scratch + 32, &0u32.to_le_bytes())
745 .map_err(crate::Error::Trap)?;
746 let hr = call_guest(
747 &mut self.cpu,
748 &mut self.mmu,
749 &self.registry,
750 &mut self.host,
751 target,
752 &[scratch, scratch + 16, scratch + 32],
753 )?;
754 if hr != crate::com::S_OK {
755 return Err(crate::Error::Win32(
756 crate::win32::Win32Error::InvalidArgument {
757 stub: "dll_get_class_object",
758 reason: format!("DllGetClassObject returned HRESULT {hr:#010x}"),
759 },
760 ));
761 }
762 let out_ptr = self.mmu.load32(scratch + 32).map_err(crate::Error::Trap)?;
763 if out_ptr == 0 {
764 return Err(crate::Error::Win32(
765 crate::win32::Win32Error::InvalidArgument {
766 stub: "dll_get_class_object",
767 reason: "DllGetClassObject succeeded but *ppv is NULL".into(),
768 },
769 ));
770 }
771 // Bookkeep the new object. If it is a class factory,
772 // also register it under `clsid` so `CoCreateInstance`
773 // can pick it up.
774 self.host.com.intern(out_ptr, Some(riid));
775 if riid == crate::com::IID_ICLASSFACTORY {
776 self.host.com.register_class_factory(clsid, out_ptr);
777 }
778 Ok(out_ptr)
779 }
780
781 /// Drive `CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER,
782 /// riid, ppv)` against the in-process class-factory cache.
783 /// The CLSID must already be registered (typically by a
784 /// prior [`Self::dll_get_class_object`] call); otherwise
785 /// surfaces `CLASS_E_CLASSNOTAVAILABLE` as an error.
786 pub fn co_create_instance(
787 &mut self,
788 clsid: crate::com::Guid,
789 riid: crate::com::Guid,
790 ) -> Result<u32, crate::Error> {
791 let factory = self.host.com.lookup_class_factory(&clsid).ok_or_else(|| {
792 crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
793 stub: "co_create_instance",
794 reason: format!(
795 "CLSID {clsid} not registered; \
796 call dll_get_class_object first"
797 ),
798 })
799 })?;
800 // Stage IID + out slot.
801 let scratch = self.host.arena_alloc(20).map_err(crate::Error::Win32)?;
802 riid.stage(&mut self.mmu, scratch)
803 .map_err(crate::Error::Trap)?;
804 self.mmu
805 .write_initializer(scratch + 16, &0u32.to_le_bytes())
806 .map_err(crate::Error::Trap)?;
807 let r = crate::com::call::call_method(
808 &mut self.cpu,
809 &mut self.mmu,
810 &self.registry,
811 &mut self.host,
812 factory,
813 crate::com::SLOT_CLASS_FACTORY_CREATE_INSTANCE,
814 &[0, scratch, scratch + 16],
815 )?;
816 if r != crate::com::S_OK {
817 return Err(crate::Error::Win32(
818 crate::win32::Win32Error::InvalidArgument {
819 stub: "co_create_instance",
820 reason: format!("CreateInstance returned HRESULT {r:#010x}"),
821 },
822 ));
823 }
824 let out = self.mmu.load32(scratch + 16).map_err(crate::Error::Trap)?;
825 if out != 0 {
826 self.host.com.intern(out, Some(riid));
827 }
828 Ok(out)
829 }
830
831 /// Drive `obj->QueryInterface(riid, ppv)` on a guest COM
832 /// object, staging the IID + out-slot in arena memory.
833 /// Returns the new interface pointer on success, or surfaces
834 /// the HRESULT in an error message.
835 pub fn query_interface(
836 &mut self,
837 obj: u32,
838 riid: crate::com::Guid,
839 ) -> Result<u32, crate::Error> {
840 let scratch = self.host.arena_alloc(20).map_err(crate::Error::Win32)?;
841 riid.stage(&mut self.mmu, scratch)
842 .map_err(crate::Error::Trap)?;
843 self.mmu
844 .write_initializer(scratch + 16, &0u32.to_le_bytes())
845 .map_err(crate::Error::Trap)?;
846 let r = crate::com::call::query_interface(
847 &mut self.cpu,
848 &mut self.mmu,
849 &self.registry,
850 &mut self.host,
851 obj,
852 scratch,
853 scratch + 16,
854 )?;
855 if r != crate::com::S_OK {
856 return Err(crate::Error::Win32(
857 crate::win32::Win32Error::InvalidArgument {
858 stub: "query_interface",
859 reason: format!("QueryInterface returned HRESULT {r:#010x}"),
860 },
861 ));
862 }
863 let out = self.mmu.load32(scratch + 16).map_err(crate::Error::Trap)?;
864 if out != 0 {
865 self.host.com.intern(out, Some(riid));
866 }
867 Ok(out)
868 }
869
870 /// Round 27 — mint a host-side `IFilterGraph` stub so the
871 /// codec's `IBaseFilter::JoinFilterGraph(pGraph, pName)` call
872 /// has a non-NULL parent graph to record. The returned guest
873 /// pointer's vtable function-pointer slots are synthetic
874 /// thunk addresses that route into the host stubs registered
875 /// by [`crate::com::host_iface::register`].
876 ///
877 /// `QueryInterface(IID_IUnknown | IID_IFilterGraph)` →
878 /// `S_OK + *ppv = obj`; every other IID returns
879 /// `E_NOINTERFACE`. All eight `IFilterGraph` methods return
880 /// `E_NOTIMPL` — none are exercised on the
881 /// `JoinFilterGraph → ReceiveConnection` path the round-27
882 /// probe takes.
883 pub fn mint_host_filter_graph(&mut self) -> Result<u32, crate::Error> {
884 crate::com::mint_host_filter_graph(&mut self.host, &mut self.mmu, &self.registry)
885 }
886
887 /// Round 27 — mint a host-side `IPin` stub that pretends to
888 /// be an OUTPUT pin advertising `amt_addr` (a pointer to a
889 /// staged `AM_MEDIA_TYPE`). Suitable as the `pConnector`
890 /// argument of `IPin::ReceiveConnection`.
891 ///
892 /// `QueryDirection` reports `PIN_OUTPUT`; `QueryAccept`
893 /// returns `S_OK`; `ConnectionMediaType` copies the staged
894 /// AMT; `EnumMediaTypes` vends an enumerator yielding the
895 /// staged AMT once.
896 pub fn mint_host_output_pin(&mut self, amt_addr: u32) -> Result<u32, crate::Error> {
897 crate::com::host_iface::mint_host_output_pin(
898 &mut self.host,
899 &mut self.mmu,
900 &self.registry,
901 amt_addr,
902 )
903 }
904
905 /// Round 37 — same as [`Self::mint_host_output_pin`] but also
906 /// stamps the codec's input-pin pointer (`connected_pin`) into
907 /// the new pin object so `IPin::ConnectedTo` can return it,
908 /// and synthesizes a parent `HostIBaseFilter` so
909 /// `IPin::QueryPinInfo` can fill in `PIN_INFO::pFilter`.
910 ///
911 /// `connected_pin == 0` falls back to the round-30 behaviour
912 /// where the pin reports `VFW_E_NOT_CONNECTED` from
913 /// `ConnectedTo`.
914 pub fn mint_host_output_pin_with_connection(
915 &mut self,
916 amt_addr: u32,
917 connected_pin: u32,
918 ) -> Result<u32, crate::Error> {
919 crate::com::host_iface::mint_host_output_pin_with_connection(
920 &mut self.host,
921 &mut self.mmu,
922 &self.registry,
923 amt_addr,
924 connected_pin,
925 )
926 }
927
928 /// Round 37 — number of `IPin::QueryPinInfo` calls the codec
929 /// has driven against any host pin during this sandbox's
930 /// lifetime.
931 pub fn query_pin_info_call_count(&self) -> usize {
932 crate::com::host_iface::query_pin_info_call_count(&self.host)
933 }
934
935 /// Round 37 — number of `IBaseFilter::QueryFilterInfo` calls
936 /// the codec has driven against any host filter during this
937 /// sandbox's lifetime.
938 pub fn query_filter_info_call_count(&self) -> usize {
939 crate::com::host_iface::query_filter_info_call_count(&self.host)
940 }
941
942 /// Round 37 — `this` pointers of every `IPin::QueryPinInfo`
943 /// call observed.
944 pub fn query_pin_info_calls(&self) -> Vec<u32> {
945 crate::com::host_iface::query_pin_info_calls(&self.host)
946 }
947
948 /// Round 37 — `this` pointers of every
949 /// `IBaseFilter::QueryFilterInfo` call observed.
950 pub fn query_filter_info_calls(&self) -> Vec<u32> {
951 crate::com::host_iface::query_filter_info_calls(&self.host)
952 }
953
954 /// Round 37 — drop every captured introspection call from this
955 /// sandbox's per-state log.
956 pub fn clear_query_info_log(&self) {
957 crate::com::host_iface::clear_query_info_log(&self.host)
958 }
959
960 /// Round 30 — mint a host-side `IMemAllocator` backed by a
961 /// pool of `pool_size` IMediaSample slots, each carrying a
962 /// fresh `sample_capacity`-byte data region. The returned
963 /// guest pointer is suitable as the `pAllocator` argument of
964 /// `IMemInputPin::NotifyAllocator`.
965 ///
966 /// `media_type_ptr` is returned by every minted sample's
967 /// `IMediaSample::GetMediaType` — pass `0` if no AMT should
968 /// surface there (codecs then fall back to the upstream pin's
969 /// connection media type).
970 pub fn mint_host_mem_allocator(
971 &mut self,
972 pool_size: u32,
973 sample_capacity: u32,
974 media_type_ptr: u32,
975 ) -> Result<u32, crate::Error> {
976 crate::com::mint_host_mem_allocator(
977 &mut self.host,
978 &mut self.mmu,
979 &self.registry,
980 pool_size,
981 sample_capacity,
982 media_type_ptr,
983 )
984 }
985
986 /// Round 35 — mint a host-side `IClassFactory` whose
987 /// `CreateInstance` mints fresh `HostIMemAllocator` instances.
988 ///
989 /// Pre-registered in [`Sandbox::new`] under
990 /// [`crate::com::CLSID_MEMORY_ALLOCATOR`]; this method exists
991 /// for tests that want a raw factory pointer to drive
992 /// `IClassFactory::CreateInstance` directly without going
993 /// through the `ole32!CoCreateInstance` cascade.
994 pub fn mint_host_mem_allocator_class_factory(&mut self) -> Result<u32, crate::Error> {
995 crate::com::mint_host_mem_allocator_class_factory(
996 &mut self.host,
997 &mut self.mmu,
998 &self.registry,
999 )
1000 }
1001
1002 /// Round 30 — mint a single host-side `IMediaSample` wrapping
1003 /// a fresh `data_capacity`-byte data region. Useful for
1004 /// stand-alone tests; production paths typically mint samples
1005 /// implicitly via [`Self::mint_host_mem_allocator`].
1006 pub fn mint_host_media_sample(
1007 &mut self,
1008 data_capacity: u32,
1009 media_type_ptr: u32,
1010 ) -> Result<u32, crate::Error> {
1011 crate::com::mint_host_media_sample(
1012 &mut self.host,
1013 &mut self.mmu,
1014 &self.registry,
1015 data_capacity,
1016 media_type_ptr,
1017 )
1018 }
1019
1020 /// Round 30 — copy a payload into a previously-minted sample
1021 /// + flag whether it is a sync (key) frame.
1022 ///
1023 /// Wraps [`crate::com::media_sample_set_payload`].
1024 pub fn media_sample_set_payload(
1025 &mut self,
1026 sample: u32,
1027 payload: &[u8],
1028 sync_point: bool,
1029 ) -> Result<(), crate::Error> {
1030 crate::com::media_sample_set_payload(&mut self.mmu, sample, payload, sync_point)
1031 }
1032
1033 /// Round 31 — mint a paired downstream `(HostIPin, HostIMemInputPin)`
1034 /// for receiving samples the codec pushes from its output pin.
1035 pub fn host_iface_r31_mint_input_pin_pair(&mut self) -> Result<(u32, u32), crate::Error> {
1036 crate::com::host_iface_r31::mint_host_input_pin_pair(
1037 &mut self.host,
1038 &mut self.mmu,
1039 &self.registry,
1040 )
1041 }
1042
1043 /// Round 31 — mint a minimal HostIBaseFilter exposing
1044 /// `input_pin`.
1045 pub fn host_iface_r31_mint_base_filter(&mut self, input_pin: u32) -> Result<u32, crate::Error> {
1046 crate::com::host_iface_r31::mint_host_base_filter(
1047 &mut self.host,
1048 &mut self.mmu,
1049 &self.registry,
1050 input_pin,
1051 )
1052 }
1053
1054 /// Round 31 — pop the oldest sample captured by the
1055 /// downstream `HostIMemInputPin::Receive` callback.
1056 pub fn pop_received_sample(&self) -> Option<crate::com::host_iface_r31::ReceivedSample> {
1057 crate::com::host_iface_r31::pop_sample(&self.host)
1058 }
1059
1060 /// Round 31 — number of samples currently waiting in the
1061 /// host-side queue.
1062 pub fn received_samples_len(&self) -> usize {
1063 crate::com::host_iface_r31::queue_len(&self.host)
1064 }
1065
1066 /// Round 33 — return the most recent
1067 /// `IMemAllocator::SetProperties` capture observed on this
1068 /// sandbox, or `None` if no codec has called `SetProperties`
1069 /// yet. See [`crate::com::AllocatorPropertiesCapture`] for
1070 /// the captured field shape.
1071 pub fn last_set_properties(&self) -> Option<crate::com::AllocatorPropertiesCapture> {
1072 crate::com::last_set_properties(&self.host)
1073 }
1074
1075 /// Round 33 — return every `SetProperties` capture observed on
1076 /// this sandbox, in arrival order.
1077 pub fn all_set_properties(&self) -> Vec<crate::com::AllocatorPropertiesCapture> {
1078 crate::com::all_set_properties(&self.host)
1079 }
1080
1081 /// Round 33 — drop every captured `SetProperties` for this
1082 /// sandbox. Useful for resetting per-test state.
1083 pub fn clear_set_properties_log(&self) {
1084 crate::com::clear_set_properties_log(&self.host)
1085 }
1086
1087 /// Drive `obj->AddRef()`. Returns the codec-reported new
1088 /// refcount; the host's bookkeeping is updated automatically.
1089 pub fn com_add_ref(&mut self, obj: u32) -> Result<u32, crate::Error> {
1090 crate::com::call::add_ref(
1091 &mut self.cpu,
1092 &mut self.mmu,
1093 &self.registry,
1094 &mut self.host,
1095 obj,
1096 )
1097 }
1098
1099 /// Drive `obj->Release()`. Returns the codec-reported new
1100 /// refcount. The host's bookkeeping is updated automatically.
1101 pub fn com_release(&mut self, obj: u32) -> Result<u32, crate::Error> {
1102 crate::com::call::release(
1103 &mut self.cpu,
1104 &mut self.mmu,
1105 &self.registry,
1106 &mut self.host,
1107 obj,
1108 )
1109 }
1110
1111 /// `ICDecompress` — decode one frame.
1112 #[allow(clippy::too_many_arguments)]
1113 pub fn ic_decompress(
1114 &mut self,
1115 hic: u32,
1116 flags: u32,
1117 input_bih: &vfw32::Bih,
1118 input_bytes: &[u8],
1119 output_bih: &vfw32::Bih,
1120 output_capacity: u32,
1121 ) -> Result<(u32, Vec<u8>), crate::Error> {
1122 vfw32::ic_decompress(
1123 &mut self.cpu,
1124 &mut self.mmu,
1125 &self.registry,
1126 &mut self.host,
1127 hic,
1128 flags,
1129 input_bih,
1130 input_bytes,
1131 output_bih,
1132 output_capacity,
1133 )
1134 }
1135
1136 // ---- Round 51: encode (compress) wrappers --------------------------
1137
1138 /// `ICCompressQuery` — does the codec accept this input/output
1139 /// format pair? `output` may be `None` to defer the choice.
1140 pub fn ic_compress_query(
1141 &mut self,
1142 hic: u32,
1143 input: &vfw32::Bih,
1144 output: Option<&vfw32::Bih>,
1145 ) -> Result<u32, crate::Error> {
1146 vfw32::ic_compress_query(
1147 &mut self.cpu,
1148 &mut self.mmu,
1149 &self.registry,
1150 &mut self.host,
1151 hic,
1152 input,
1153 output,
1154 )
1155 }
1156
1157 /// `ICCompressGetFormat` — ask the codec for the output BIH
1158 /// describing what its compressed format looks like for the
1159 /// supplied input.
1160 pub fn ic_compress_get_format(
1161 &mut self,
1162 hic: u32,
1163 input: &vfw32::Bih,
1164 ) -> Result<(u32, vfw32::Bih), crate::Error> {
1165 vfw32::ic_compress_get_format(
1166 &mut self.cpu,
1167 &mut self.mmu,
1168 &self.registry,
1169 &mut self.host,
1170 hic,
1171 input,
1172 )
1173 }
1174
1175 /// `ICCompressGetSize` — max encoded-frame byte count for the
1176 /// supplied input/output BIH pair.
1177 pub fn ic_compress_get_size(
1178 &mut self,
1179 hic: u32,
1180 input: &vfw32::Bih,
1181 output: &vfw32::Bih,
1182 ) -> Result<u32, crate::Error> {
1183 vfw32::ic_compress_get_size(
1184 &mut self.cpu,
1185 &mut self.mmu,
1186 &self.registry,
1187 &mut self.host,
1188 hic,
1189 input,
1190 output,
1191 )
1192 }
1193
1194 /// `ICCompressBegin` — set up the encoder pipeline.
1195 pub fn ic_compress_begin(
1196 &mut self,
1197 hic: u32,
1198 input: &vfw32::Bih,
1199 output: &vfw32::Bih,
1200 ) -> Result<u32, crate::Error> {
1201 vfw32::ic_compress_begin(
1202 &mut self.cpu,
1203 &mut self.mmu,
1204 &self.registry,
1205 &mut self.host,
1206 hic,
1207 input,
1208 output,
1209 )
1210 }
1211
1212 /// `ICCompressEnd` — tear down the encoder pipeline.
1213 pub fn ic_compress_end(&mut self, hic: u32) -> Result<u32, crate::Error> {
1214 vfw32::ic_compress_end(
1215 &mut self.cpu,
1216 &mut self.mmu,
1217 &self.registry,
1218 &mut self.host,
1219 hic,
1220 )
1221 }
1222
1223 /// `ICCompress` — encode one frame. Returns the full encode
1224 /// outcome: codec LRESULT, encoded bytes, the post-call output
1225 /// BIH (whose `biSizeImage` holds the actual encoded byte
1226 /// count), the codec-written `*lpdwFlags` (e.g. whether the
1227 /// codec marked the emitted frame as a keyframe), and the
1228 /// codec-written `*lpckid`.
1229 ///
1230 /// `prev_bih_opt` / `prev_bytes_opt` are the previous
1231 /// reconstructed frame (P-frame encoding). Pass `None` for
1232 /// keyframes.
1233 #[allow(clippy::too_many_arguments)]
1234 pub fn ic_compress(
1235 &mut self,
1236 hic: u32,
1237 flags: u32,
1238 input_bih: &vfw32::Bih,
1239 input_bytes: &[u8],
1240 output_bih: &vfw32::Bih,
1241 output_capacity: u32,
1242 ckid: u32,
1243 frame_num: i32,
1244 frame_size_limit: u32,
1245 quality: u32,
1246 prev_bih_opt: Option<&vfw32::Bih>,
1247 prev_bytes_opt: Option<&[u8]>,
1248 ) -> Result<vfw32::CompressOutcome, crate::Error> {
1249 vfw32::ic_compress(
1250 &mut self.cpu,
1251 &mut self.mmu,
1252 &self.registry,
1253 &mut self.host,
1254 hic,
1255 flags,
1256 input_bih,
1257 input_bytes,
1258 output_bih,
1259 output_capacity,
1260 ckid,
1261 frame_num,
1262 frame_size_limit,
1263 quality,
1264 prev_bih_opt,
1265 prev_bytes_opt,
1266 )
1267 }
1268
1269 /// `ICGetState` — ask the codec to serialise its private
1270 /// per-instance state into `dst_buf`. Returns the byte count
1271 /// the codec actually wrote.
1272 ///
1273 /// Round 70 — wraps `ICM_GETSTATE` (`0x5009`) per MSDN; required
1274 /// by oxideav-tracevfw to drive the encoder's per-quality knob
1275 /// round-trip alongside [`Self::ic_set_state`]. See MSDN
1276 /// `ICGetState` topic page for the public contract.
1277 pub fn ic_get_state(&mut self, hic: u32, dst_buf: &mut [u8]) -> Result<u32, crate::Error> {
1278 vfw32::ic_get_state(
1279 &mut self.cpu,
1280 &mut self.mmu,
1281 &self.registry,
1282 &mut self.host,
1283 hic,
1284 dst_buf,
1285 )
1286 }
1287
1288 /// `ICSetState` — ask the codec to deserialise `src_buf` into
1289 /// its private per-instance state. Returns `Ok(())` on
1290 /// `ICERR_OK`, or [`crate::Error`] wrapping the codec's raw
1291 /// `LRESULT` otherwise.
1292 ///
1293 /// Round 70 — wraps `ICM_SETSTATE` (`0x500A`) per MSDN. See
1294 /// MSDN `ICSetState` topic page for the public contract.
1295 pub fn ic_set_state(&mut self, hic: u32, src_buf: &[u8]) -> Result<(), crate::Error> {
1296 vfw32::ic_set_state(
1297 &mut self.cpu,
1298 &mut self.mmu,
1299 &self.registry,
1300 &mut self.host,
1301 hic,
1302 src_buf,
1303 )
1304 }
1305
1306 /// Round 63 — patch `msadds32.ax`'s `helper_addref` thunk (at
1307 /// RVA `0x5cea`) to unconditionally return `value` (32-bit
1308 /// integer).
1309 ///
1310 /// **Why.** Round-62 forensics
1311 /// (`docs/codec/msadds32-receive-null-0x20.md`) traced the
1312 /// `IMemInputPin::Receive` NULL-deref trap at RVA `0x256a` to
1313 /// a buffer-pool init that's handed a size of zero. The size
1314 /// is `(h * 10) / size_calc(...)` where `h` is what
1315 /// `helper_addref` returns. On a fresh codec instance the
1316 /// helper-object field at `helper_90 + 0x3c` (the "initialised"
1317 /// flag the addref checks) is zero, so `helper_addref` returns
1318 /// `0`, the quotient is `0`, `operator new(0)` returns NULL,
1319 /// `buffer_pool_init` fails, and the Receive cleanup branch
1320 /// trips a NULL+0x20 deref.
1321 ///
1322 /// In a real DirectShow host the flag is set during
1323 /// `IFilterGraph::JoinFilterGraph` / `Pause` (the codec stamps
1324 /// the field as part of its run-state machine). Until we
1325 /// drive that path, the surgical workaround is to short-circuit
1326 /// `helper_addref` to return a fixed non-zero value — which
1327 /// empirically lifts the trap and lets Receive run to
1328 /// completion (with HRESULT `0x8000ffff` from the decode body,
1329 /// which is the next round's investigation surface).
1330 ///
1331 /// **Encoding.** The original function (RVA `0x5cea`, 10 bytes)
1332 /// is:
1333 ///
1334 /// ```text
1335 /// 0x5cea: 83 79 20 00 cmp [ecx+0x20], 0
1336 /// 0x5cee: 74 04 jz +4
1337 /// 0x5cf0: 8b 41 28 mov eax, [ecx+0x28]
1338 /// 0x5cf3: c3 ret
1339 /// 0x5cf4: 33 c0 xor eax, eax
1340 /// 0x5cf6: c3 ret
1341 /// ```
1342 ///
1343 /// We overwrite the first 6 bytes with:
1344 ///
1345 /// ```text
1346 /// b8 XX XX XX XX mov eax, imm32
1347 /// c3 ret
1348 /// ```
1349 ///
1350 /// The remaining bytes at `0x5cf0..0x5cf6` are unreachable
1351 /// after the patch (no caller enters there directly), so the
1352 /// dead-code is harmless.
1353 ///
1354 /// `image_base` is the address the codec was loaded at; pass
1355 /// the value returned by [`Self::load`] on `msadds32.ax`.
1356 ///
1357 /// # Reference material (clean-room only)
1358 ///
1359 /// * Intel SDM Vol. 2A — `MOV imm32` (`B8+rd`), `RET` (`C3`).
1360 /// * Raw bytes of `msadds32.ax` from
1361 /// `docs/video/msmpeg4/reference/binaries/wmpcdcs8-2001/`.
1362 ///
1363 /// No Wine / ReactOS / MinGW / Microsoft DShow source consulted.
1364 pub fn msadds32_patch_helper_addref(
1365 &mut self,
1366 image_base: u32,
1367 value: u32,
1368 ) -> Result<(), crate::Error> {
1369 const RVA_HELPER_ADDREF: u32 = 0x5cea;
1370 let va = image_base.wrapping_add(RVA_HELPER_ADDREF);
1371 let mut patch = [0u8; 6];
1372 patch[0] = 0xb8; // mov eax, imm32
1373 patch[1..5].copy_from_slice(&value.to_le_bytes());
1374 patch[5] = 0xc3; // ret
1375 self.mmu
1376 .write_initializer(va, &patch)
1377 .map_err(crate::Error::Trap)
1378 }
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383 use super::*;
1384 use crate::emulator::isa_int::RET_SENTINEL;
1385 use crate::emulator::regs::Reg32;
1386 use crate::pe::test_image::build_minimal_dll;
1387
1388 #[test]
1389 fn load_synth_dll_and_run_dll_main_returns_to_sentinel() {
1390 let bytes = build_minimal_dll();
1391 let mut sb = Sandbox::new();
1392 let img = sb.load("synth.dll", &bytes).unwrap();
1393 // Pre-set eax = 1 so we can confirm the synth DllMain
1394 // returned without modifying it (it's just `ret 12`).
1395 sb.cpu.regs.set32(Reg32::Eax, 1);
1396 let ret = sb.call_dll_main(&img, DLL_PROCESS_ATTACH).unwrap();
1397 assert_eq!(ret, 1);
1398 assert_eq!(sb.cpu.regs.eip, RET_SENTINEL);
1399 }
1400
1401 #[test]
1402 fn calling_through_iat_thunk_invokes_kernel32_stub() {
1403 // Emulator-only test: fabricate a code block that calls
1404 // a kernel32!GetProcessHeap thunk and rets. Verifies the
1405 // run loop's "is_thunk → dispatch" path.
1406 let mut sb = Sandbox::new();
1407 let thunk = sb
1408 .registry
1409 .resolve("kernel32.dll", "GetProcessHeap")
1410 .unwrap();
1411 // Map a code page at 0x1000.
1412 sb.mmu.map(0x1000, 0x1000, Perm::R | Perm::X);
1413 // call dword [thunk_slot]; ret 0
1414 // Easier: set eip directly to the thunk after pushing
1415 // the synthetic ret-sentinel.
1416 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1417 sb.cpu.regs.eip = thunk;
1418 sb.run_until_sentinel().unwrap();
1419 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 0xDEAD_BEEF);
1420 }
1421
1422 /// Phase 3c of the scheduler refactor: `WaitForSingleObject`
1423 /// on an unsignalled auto-reset Event parks the calling
1424 /// thread; `SetEvent` from a peer wakes exactly one waiter.
1425 /// We exercise this directly through state mutation rather
1426 /// than spawning guest threads (the kernel32 thunk path is
1427 /// covered by the spawn test below).
1428 #[test]
1429 fn wait_for_single_object_blocks_until_set_event() {
1430 let mut sb = Sandbox::new();
1431 // Mint an auto-reset Event, initially unsignalled.
1432 let h = sb
1433 .host
1434 .scheduler
1435 .insert_object(crate::sched::WaitObject::Event {
1436 signaled: false,
1437 manual_reset: false,
1438 });
1439 // Inject a second thread parked on a wait for the event.
1440 let mut t2 = crate::win32::ThreadState::new(2, 1);
1441 t2.parked_cpu = Some(crate::emulator::Cpu::new());
1442 t2.status = crate::sched::ThreadStatus::Waiting;
1443 t2.wait = Some(crate::sched::WaitCondition::Object {
1444 handle: h,
1445 timeout_after: None,
1446 });
1447 sb.host.threads.insert(2, t2);
1448
1449 // Bootstrap drives SetEvent via the kernel32 thunk.
1450 let set_event = sb.registry.resolve("kernel32.dll", "SetEvent").unwrap();
1451 sb.cpu.push32(&mut sb.mmu, h).unwrap();
1452 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1453 sb.cpu.regs.eip = set_event;
1454 sb.run_until_sentinel().unwrap();
1455
1456 // Thread 2 should be back to Ready with no wait.
1457 let t = sb.host.threads.get(&2).unwrap();
1458 assert!(
1459 matches!(t.status, crate::sched::ThreadStatus::Ready),
1460 "expected thread 2 Ready, got {:?}",
1461 t.status
1462 );
1463 assert!(t.wait.is_none(), "wait condition must be cleared");
1464 // Auto-reset: the signal is consumed by the wake.
1465 match sb.host.scheduler.objects.get(&h).unwrap() {
1466 crate::sched::WaitObject::Event { signaled, .. } => assert!(!*signaled),
1467 _ => panic!(),
1468 }
1469 }
1470
1471 /// Mutex round-trip: contention parks the second caller,
1472 /// `ReleaseMutex` from the holder wakes them and transfers
1473 /// ownership.
1474 #[test]
1475 fn mutex_transfers_ownership_on_release() {
1476 let mut sb = Sandbox::new();
1477 // Mint a Mutex owned by tid 1 (the bootstrap).
1478 let h = sb
1479 .host
1480 .scheduler
1481 .insert_object(crate::sched::WaitObject::Mutex {
1482 owner: Some(1),
1483 recursion: 1,
1484 });
1485 // Inject thread 2 waiting on the mutex.
1486 let mut t2 = crate::win32::ThreadState::new(2, 1);
1487 t2.parked_cpu = Some(crate::emulator::Cpu::new());
1488 t2.status = crate::sched::ThreadStatus::Waiting;
1489 t2.wait = Some(crate::sched::WaitCondition::Object {
1490 handle: h,
1491 timeout_after: None,
1492 });
1493 sb.host.threads.insert(2, t2);
1494
1495 // Bootstrap calls ReleaseMutex.
1496 let release = sb.registry.resolve("kernel32.dll", "ReleaseMutex").unwrap();
1497 sb.cpu.push32(&mut sb.mmu, h).unwrap();
1498 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1499 sb.cpu.regs.eip = release;
1500 sb.run_until_sentinel().unwrap();
1501 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1, "release succeeds");
1502
1503 // Ownership transferred to thread 2; thread 2 Ready.
1504 match sb.host.scheduler.objects.get(&h).unwrap() {
1505 crate::sched::WaitObject::Mutex { owner, recursion } => {
1506 assert_eq!(*owner, Some(2));
1507 assert_eq!(*recursion, 1);
1508 }
1509 _ => panic!(),
1510 }
1511 let t = sb.host.threads.get(&2).unwrap();
1512 assert!(matches!(t.status, crate::sched::ThreadStatus::Ready));
1513 }
1514
1515 /// Semaphore round-trip: `ReleaseSemaphore(N)` wakes up to
1516 /// N waiters and bumps the count.
1517 #[test]
1518 fn semaphore_release_wakes_waiters_and_bumps_count() {
1519 let mut sb = Sandbox::new();
1520 let h = sb
1521 .host
1522 .scheduler
1523 .insert_object(crate::sched::WaitObject::Semaphore { count: 0, max: 10 });
1524 // Two waiters.
1525 for tid in [2u32, 3u32] {
1526 let mut t = crate::win32::ThreadState::new(tid, 1);
1527 t.parked_cpu = Some(crate::emulator::Cpu::new());
1528 t.status = crate::sched::ThreadStatus::Waiting;
1529 t.wait = Some(crate::sched::WaitCondition::Object {
1530 handle: h,
1531 timeout_after: None,
1532 });
1533 sb.host.threads.insert(tid, t);
1534 }
1535 // Bootstrap releases 2.
1536 let release = sb
1537 .registry
1538 .resolve("kernel32.dll", "ReleaseSemaphore")
1539 .unwrap();
1540 sb.mmu.map(0x500_0000, 0x1000, Perm::R | Perm::W);
1541 let p_prev = 0x500_0000u32;
1542 sb.cpu.push32(&mut sb.mmu, p_prev).unwrap();
1543 sb.cpu.push32(&mut sb.mmu, 2u32).unwrap();
1544 sb.cpu.push32(&mut sb.mmu, h).unwrap();
1545 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1546 sb.cpu.regs.eip = release;
1547 sb.run_until_sentinel().unwrap();
1548 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1549 // Previous count was 0.
1550 assert_eq!(sb.mmu.load32(p_prev).unwrap(), 0);
1551 // Both waiters woke; each consumed one count → net count = 0.
1552 match sb.host.scheduler.objects.get(&h).unwrap() {
1553 crate::sched::WaitObject::Semaphore { count, .. } => {
1554 assert_eq!(*count, 0, "both waiters consumed their signals");
1555 }
1556 _ => panic!(),
1557 }
1558 for tid in [2u32, 3u32] {
1559 let t = sb.host.threads.get(&tid).unwrap();
1560 assert!(matches!(t.status, crate::sched::ThreadStatus::Ready));
1561 }
1562 }
1563
1564 /// Phase 5c: when the target EXE is staged in the
1565 /// VirtualFs, `CreateProcessA` actually loads it at a
1566 /// fresh image base and mints a Ready primary thread the
1567 /// scheduler can run alongside the parent. The PE used
1568 /// here is the synthetic-DLL fixture — it has a real PE
1569 /// header and the only import (`kernel32!ExitProcess`) is
1570 /// satisfied by our stub registry.
1571 #[test]
1572 fn create_process_a_loads_real_child_pe_from_vfs() {
1573 use crate::pe::test_image::build_minimal_dll;
1574 let mut sb = Sandbox::default();
1575 // Stage a child binary in the VFS.
1576 let dll_bytes = build_minimal_dll();
1577 let child_path = "c:\\setup\\helper.exe";
1578 let mut vfs = crate::context::VirtualFs::new();
1579 vfs.insert(child_path, dll_bytes);
1580 sb.host.context.vfs = Some(vfs);
1581
1582 // Stage a PROCESS_INFORMATION scratch region.
1583 sb.mmu.map(0x500_0000, 0x1000, Perm::R | Perm::W);
1584 let pi = 0x500_0000u32;
1585 // Stage the lpApplicationName string.
1586 sb.mmu.map(0x500_1000, 0x1000, Perm::R | Perm::W);
1587 let app_ptr = 0x500_1000u32;
1588 sb.mmu
1589 .write_initializer(app_ptr, child_path.as_bytes())
1590 .unwrap();
1591 sb.mmu.store8(app_ptr + child_path.len() as u32, 0).unwrap();
1592
1593 let create_proc = sb
1594 .registry
1595 .resolve("kernel32.dll", "CreateProcessA")
1596 .unwrap();
1597 for &a in [app_ptr, 0u32, 0, 0, 0, 0, 0, 0, 0, pi].iter().rev() {
1598 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1599 }
1600 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1601 sb.cpu.regs.eip = create_proc;
1602 sb.run_until_sentinel().unwrap();
1603 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1, "CreateProcessA TRUE");
1604
1605 // The new process should be in the table, distinct PID,
1606 // and its primary thread Ready (not Terminated like the
1607 // fake-child fallback).
1608 let child_pid = sb.mmu.load32(pi + 8).unwrap();
1609 let child_tid = sb.mmu.load32(pi + 12).unwrap();
1610 let p = sb
1611 .host
1612 .processes
1613 .get(&child_pid)
1614 .expect("child process present");
1615 assert_ne!(
1616 p.image_base, 0,
1617 "child PE was rebased into a fresh image slot"
1618 );
1619 assert_eq!(p.parent_pid, 1);
1620 let t = sb
1621 .host
1622 .threads
1623 .get(&child_tid)
1624 .expect("child primary thread present");
1625 assert!(matches!(
1626 t.status,
1627 crate::sched::ThreadStatus::Ready | crate::sched::ThreadStatus::Running
1628 ));
1629 // The minimal-DLL entry just rets, so a subsequent
1630 // run_until_sentinel will Halt it cleanly.
1631 }
1632
1633 /// Phase 3d: `EnterCriticalSection` takes ownership of an
1634 /// implicit Mutex keyed by `LPCRITICAL_SECTION`'s guest
1635 /// address; recursion increments on re-entry by the same
1636 /// thread; `LeaveCriticalSection` decrements and clears
1637 /// ownership when the count reaches zero. Exercised through
1638 /// IPC: `CreatePipe` mints a read/write handle pair;
1639 /// `WriteFile` on the write end stages bytes that
1640 /// `ReadFile` on the read end picks up. The two handles
1641 /// share an in-memory buffer through the scheduler's
1642 /// pipe table; closing the write end drains EOF on the
1643 /// reader.
1644 #[test]
1645 fn create_pipe_write_read_round_trip() {
1646 let mut sb = Sandbox::new();
1647 // Scratch region for handle outputs + data buffers.
1648 sb.mmu.map(0x500_0000, 0x1000, Perm::R | Perm::W);
1649 let p_read_h = 0x500_0000u32;
1650 let p_write_h = 0x500_0004u32;
1651 let p_written = 0x500_0010u32;
1652 let p_data_in = 0x500_0020u32; // bytes to write
1653 let p_data_out = 0x500_0040u32; // bytes to read into
1654 let p_n_read = 0x500_0080u32;
1655
1656 // CreatePipe(read, write, NULL, 0).
1657 let create_pipe = sb.registry.resolve("kernel32.dll", "CreatePipe").unwrap();
1658 for &a in [p_read_h, p_write_h, 0u32, 0u32].iter().rev() {
1659 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1660 }
1661 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1662 sb.cpu.regs.eip = create_pipe;
1663 sb.run_until_sentinel().unwrap();
1664 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1, "CreatePipe TRUE");
1665 let h_read = sb.mmu.load32(p_read_h).unwrap();
1666 let h_write = sb.mmu.load32(p_write_h).unwrap();
1667 assert!(h_read >= crate::sched::WAIT_OBJECT_HANDLE_BASE);
1668 assert_ne!(h_read, h_write, "distinct ends");
1669
1670 // Stage data in MMU + WriteFile(write_handle, data, 5, &written, NULL).
1671 sb.mmu.write_initializer(p_data_in, b"HELLO").unwrap();
1672 let write_file = sb.registry.resolve("kernel32.dll", "WriteFile").unwrap();
1673 for &a in [h_write, p_data_in, 5u32, p_written, 0u32].iter().rev() {
1674 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1675 }
1676 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1677 sb.cpu.regs.eip = write_file;
1678 sb.run_until_sentinel().unwrap();
1679 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1680 assert_eq!(sb.mmu.load32(p_written).unwrap(), 5);
1681
1682 // ReadFile(read_handle, out_buf, 5, &n_read, NULL).
1683 let read_file = sb.registry.resolve("kernel32.dll", "ReadFile").unwrap();
1684 for &a in [h_read, p_data_out, 5u32, p_n_read, 0u32].iter().rev() {
1685 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1686 }
1687 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1688 sb.cpu.regs.eip = read_file;
1689 sb.run_until_sentinel().unwrap();
1690 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1691 assert_eq!(sb.mmu.load32(p_n_read).unwrap(), 5);
1692 // Verify the bytes round-tripped.
1693 let mut got = [0u8; 5];
1694 for i in 0..5 {
1695 got[i] = sb.mmu.load8(p_data_out + i as u32).unwrap();
1696 }
1697 assert_eq!(&got, b"HELLO");
1698 }
1699
1700 /// Named-pipe handshake: a `CreateNamedPipeA(\\.\pipe\X)`
1701 /// server end + a `CreateFileA(\\.\pipe\X)` client end
1702 /// reach each other through the named-object registry
1703 /// and share a buffer for `WriteFile` / `ReadFile`.
1704 #[test]
1705 fn named_pipe_server_and_client_share_buffer() {
1706 let mut sb = Sandbox::new();
1707 sb.mmu.map(0x501_0000, 0x1000, Perm::R | Perm::W);
1708 let p_name = 0x501_0000u32;
1709 let pipe_path = b"\\\\.\\pipe\\test\0";
1710 sb.mmu.write_initializer(p_name, pipe_path).unwrap();
1711 let p_data_in = 0x501_0100u32;
1712 let p_data_out = 0x501_0200u32;
1713 let p_written = 0x501_0300u32;
1714 let p_n_read = 0x501_0310u32;
1715
1716 // CreateNamedPipeA(name, PIPE_ACCESS_OUTBOUND, ...). With
1717 // OUTBOUND, the server holds the write end.
1718 let cnp = sb
1719 .registry
1720 .resolve("kernel32.dll", "CreateNamedPipeA")
1721 .unwrap();
1722 for &a in [p_name, 2u32, 0u32, 1u32, 4096u32, 4096u32, 0u32, 0u32]
1723 .iter()
1724 .rev()
1725 {
1726 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1727 }
1728 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1729 sb.cpu.regs.eip = cnp;
1730 sb.run_until_sentinel().unwrap();
1731 let h_server = sb.cpu.regs.get32(Reg32::Eax);
1732 assert!(h_server >= crate::sched::WAIT_OBJECT_HANDLE_BASE);
1733
1734 // Client: CreateFileA(\\.\pipe\test, GENERIC_READ, ...).
1735 let cfa = sb.registry.resolve("kernel32.dll", "CreateFileA").unwrap();
1736 for &a in [p_name, 0x8000_0000u32, 0u32, 0u32, 3u32, 0u32, 0u32]
1737 .iter()
1738 .rev()
1739 {
1740 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1741 }
1742 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1743 sb.cpu.regs.eip = cfa;
1744 sb.run_until_sentinel().unwrap();
1745 let h_client = sb.cpu.regs.get32(Reg32::Eax);
1746 assert_ne!(h_client, 0xFFFF_FFFF, "client open should succeed");
1747 assert_ne!(h_client, h_server, "distinct handles");
1748
1749 // Server WriteFile.
1750 sb.mmu.write_initializer(p_data_in, b"PING!").unwrap();
1751 let wfile = sb.registry.resolve("kernel32.dll", "WriteFile").unwrap();
1752 for &a in [h_server, p_data_in, 5u32, p_written, 0u32].iter().rev() {
1753 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1754 }
1755 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1756 sb.cpu.regs.eip = wfile;
1757 sb.run_until_sentinel().unwrap();
1758 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1759 assert_eq!(sb.mmu.load32(p_written).unwrap(), 5);
1760
1761 // Client ReadFile.
1762 let rfile = sb.registry.resolve("kernel32.dll", "ReadFile").unwrap();
1763 for &a in [h_client, p_data_out, 5u32, p_n_read, 0u32].iter().rev() {
1764 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1765 }
1766 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1767 sb.cpu.regs.eip = rfile;
1768 sb.run_until_sentinel().unwrap();
1769 assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1770 assert_eq!(sb.mmu.load32(p_n_read).unwrap(), 5);
1771 let mut got = [0u8; 5];
1772 for i in 0..5 {
1773 got[i] = sb.mmu.load8(p_data_out + i as u32).unwrap();
1774 }
1775 assert_eq!(&got, b"PING!");
1776 }
1777
1778 /// the kernel32 thunk dispatch.
1779 #[test]
1780 fn enter_leave_critical_section_round_trip() {
1781 let mut sb = Sandbox::new();
1782 // Map a CRITICAL_SECTION scratch region.
1783 sb.mmu.map(0x400_0000, 0x1000, Perm::R | Perm::W);
1784 let cs = 0x400_0000u32;
1785 let enter = sb
1786 .registry
1787 .resolve("kernel32.dll", "EnterCriticalSection")
1788 .unwrap();
1789 let leave = sb
1790 .registry
1791 .resolve("kernel32.dll", "LeaveCriticalSection")
1792 .unwrap();
1793 // First enter: take ownership, recursion=1.
1794 sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1795 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1796 sb.cpu.regs.eip = enter;
1797 sb.run_until_sentinel().unwrap();
1798 let h = sb.host.scheduler.critical_sections[&cs];
1799 match sb.host.scheduler.objects.get(&h).unwrap() {
1800 crate::sched::WaitObject::CriticalSection {
1801 owner, recursion, ..
1802 } => {
1803 assert_eq!(*owner, Some(1));
1804 assert_eq!(*recursion, 1);
1805 }
1806 _ => panic!(),
1807 }
1808 // Re-entry by same thread: recursion=2.
1809 sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1810 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1811 sb.cpu.regs.eip = enter;
1812 sb.run_until_sentinel().unwrap();
1813 match sb.host.scheduler.objects.get(&h).unwrap() {
1814 crate::sched::WaitObject::CriticalSection { recursion, .. } => {
1815 assert_eq!(*recursion, 2);
1816 }
1817 _ => panic!(),
1818 }
1819 // First leave: recursion→1.
1820 sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1821 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1822 sb.cpu.regs.eip = leave;
1823 sb.run_until_sentinel().unwrap();
1824 // Second leave: recursion→0, owner cleared.
1825 sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1826 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1827 sb.cpu.regs.eip = leave;
1828 sb.run_until_sentinel().unwrap();
1829 match sb.host.scheduler.objects.get(&h).unwrap() {
1830 crate::sched::WaitObject::CriticalSection {
1831 owner, recursion, ..
1832 } => {
1833 assert_eq!(*owner, None);
1834 assert_eq!(*recursion, 0);
1835 }
1836 _ => panic!(),
1837 }
1838 }
1839
1840 /// Phase 3b of the scheduler refactor: `CreateThread` mints
1841 /// a Ready thread, the scheduler context-switches into it
1842 /// the first time the bootstrap thread yields, and the new
1843 /// thread runs to its own RET_SENTINEL where it
1844 /// terminates. Verifies both halves of the round trip.
1845 #[test]
1846 fn create_thread_spawns_a_real_runnable_thread() {
1847 let mut sb = Sandbox::new();
1848 // Map a code page for the synthetic thread proc.
1849 sb.mmu.map(0x1000, 0x1000, Perm::R | Perm::X);
1850 // Thread proc: `mov eax, 0x42; ret 4` (stdcall, one arg
1851 // consumed; eax = exit code).
1852 let proc_va = 0x1010u32;
1853 let proc_code: [u8; 8] = [
1854 0xb8, 0x42, 0x00, 0x00, 0x00, // mov eax, 0x42
1855 0xc2, 0x04, 0x00, // ret 4
1856 ];
1857 sb.mmu.write_initializer(proc_va, &proc_code).unwrap();
1858 // Drive CreateThread via the kernel32 thunk so we exercise
1859 // the same code path a real codec would.
1860 let create_thread = sb.registry.resolve("kernel32.dll", "CreateThread").unwrap();
1861 // Push args right-to-left: (attrs, stack, start, param,
1862 // flags, p_tid) = (0, 0, proc_va, 0xCAFE, 0, 0).
1863 for &a in [0u32, 0, proc_va, 0xCAFE, 0, 0].iter().rev() {
1864 sb.cpu.push32(&mut sb.mmu, a).unwrap();
1865 }
1866 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1867 sb.cpu.regs.eip = create_thread;
1868 sb.run_until_sentinel().unwrap();
1869 let thread_handle = sb.cpu.regs.get32(Reg32::Eax);
1870 assert!(thread_handle >= crate::sched::WAIT_OBJECT_HANDLE_BASE);
1871 // The new thread is in the table, Ready, with its CPU
1872 // parked. TID 2 (bootstrap is 1).
1873 let t = sb.host.threads.get(&2).expect("new thread present");
1874 assert!(matches!(t.status, crate::sched::ThreadStatus::Ready));
1875 assert!(t.parked_cpu.is_some());
1876 assert_ne!(
1877 t.parked_cpu.as_ref().unwrap().regs.esp(),
1878 0,
1879 "stack was carved from the pool"
1880 );
1881
1882 // Bootstrap now Sleeps — the scheduler must context
1883 // switch into the new thread, run it to its
1884 // RET_SENTINEL, mark it Terminated, and then come
1885 // back to the bootstrap (which wakes after the sleep
1886 // clock advances).
1887 let sleep_thunk = sb.registry.resolve("kernel32.dll", "Sleep").unwrap();
1888 // Sleep(1ms)
1889 sb.cpu.push32(&mut sb.mmu, 1u32).unwrap();
1890 sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1891 sb.cpu.regs.eip = sleep_thunk;
1892 sb.run_until_sentinel().unwrap();
1893 // After the run, the new thread should have ran to
1894 // completion (status Terminated, eax = 0x42 on its
1895 // parked CPU if any).
1896 let t = sb.host.threads.get(&2).expect("new thread still present");
1897 assert!(
1898 matches!(t.status, crate::sched::ThreadStatus::Terminated),
1899 "expected thread 2 Terminated, got {:?}",
1900 t.status
1901 );
1902 }
1903}