CRTDLL _iob static data identity

2026-10-18 compiled validation complete. No new game run.

Original Circus Linux 1.0.0's _iob import at IAT VA 004b7340 is used as a FILE-array address: stdout +32, stderr +64. The post-fpreset run passes 07500050 to setbuf, implying a wrongly thunk-backed _iob base 07500010. SDL WinMain attempts freopen and, on failure, copies eight dwords from fopen's FILE into those addresses. Evidence is retained in the shared repository under scratch/runs/20261018-circuslinux-stdio-source/ and scratch/runs/20261018-circuslinux-fpreset-retry/.

Wine's upstream CRTDLL export specification marks _iob as an extern data export, consistent with the original binary's direct dereference and 32-byte structure stride. This is implementation/source evidence, not a new native Windows execution oracle.

Bounded fix

Both the main PE import path and a loaded caller's system-DLL import path now recognize exact _iob from case-insensitive crtdll.dll. They use the existing $crt_iob_ensure process-shared publisher, not a new array or a callable thunk. The reserved thunk slot is retained so subsequent import indices stay stable. No API table entry or dispatch generation changes are needed. Other modules or symbols are left to their existing resolution paths.

The PE loader initializes the heap before processing imports. _iob creation allocates 96 bytes for three 32-byte FILEs and 144 bytes for ownership records; initial flags are read/write/write and descriptors 0/1/2. Publication is under the existing CRT list lock with an atomic pointer, and losing creators free their allocations. Every thread instance sees the same pointer and list. Allocation failure traps rather than silently resolving this data symbol to a function thunk. Reopened standard streams retain their FILE addresses and registered ownership. No stdio buffering behavior changes.

The DLL-loader branch for real mapped dependencies remains before the system fallback and resolves the real export. EXE imports can subsequently be patched by the existing native dependency pass (patchExeImportsLocked calls patch_caller_iat against matching mapped modules). The synthetic table is only the builtin CRT route; this change does not substitute for real DLL exports.

Dynamic GetProcAddress is deliberately outside this static-import patch: CRTDLL lacks a unique synthetic handle and aliases the EXE handle today. A global _iob name override would misidentify unrelated modules. A future dynamic fix needs module identity first. Full-path DLL spellings and other CRT families are also outside this exact-basename rule. The historical callable _iob API remains for existing callers/tests. setbuf remains fail-fast. Its true custom buffer behavior requires a separately scoped stdio buffering implementation.

Conformance and validation

test/test-crtdll-iob-data-import.js uses a minimal PE with the same direct stderr dereference. It checks the IAT points outside the thunk range, ordinary import slots stay stable, all three FILEs are owned and have expected flags and descriptor offsets, EXE/DLL paths share identity, foreign module/symbol suffixes are rejected, sibling instances share the array, and freopen/fputs accept the imported stderr address with actual VFS output. A second guest process with distinct linear memory proves FILE mutations and VFS contents stay isolated. A real mapped PE CRTDLL fixture exporting _iob proves a subsequently loaded caller's import resolves to that native data export rather than the builtin array. Shared-instance checks are sequential; this does not establish concurrent publication behavior or reset behavior when reusing a process memory.

The following commands passed (exit 0) in isolated worktree /tmp/wa-circus-iob-20261018, based on origin/main 97d5f10890d7c8ceaca8fe5f7641d3c4906c3422, with implementation commit e7e443da81c76482bd3e5a95f40ca38f001b07a6 and the added test coverage:

bash tools/build.sh
node test/test-crtdll-iob-data-import.js
node test/test-crt-stdio-file.js
node test/test-msvcrt-acmdln-data-import.js
node test/test-crt-mkdir-setvbuf-getc.js

The normal build passed all gates; candidate module is 1,866,594 bytes, SHA-256 1505f311d2d6edc98c184bfcf9093485036db0ec55a590e00b655c6fffbf3afa. The canonical integration module remains unchanged at 0c617764e036d0422ab19bc0d97477ff68ba5eb8fa7c247833b7519e8060e5f7. Logs and individual command receipts are retained under scratch/runs/20261018-crtdll-iob-validation/ in the shared repository. Each command used a detached process group with a 180-second deadline, 8-MiB log cap, and strict 2-GiB plus 64-MiB disk stop margin. Minimum observed free space was 2,396,475,392 bytes; no guard fired. All five child processes exited normally. No game, browser, setbuf substitution, or runtime retry was run.