CRT _fdopen: descriptor ownership checkpoint
2026-10-18, source-only audit after Mad Bomber's original zlib packed loader
received NULL for CRTDLL _fdopen. See the original-game evidence.
This checkpoint deliberately does not register an API that cannot yet meet
the requested descriptor/access/mode contract. No handler, fake FILE, guest
replacement, build, compiled test or new game run is included.
Decision and scope boundary
The existing FILE representation is suitable for wrapping a descriptor, but
a handler-only change cannot validate that descriptor's access and mode from
the current WAT-visible state. A correct implementation needs a descriptor
metadata prerequisite across _open, _dup, _close and stream adoption,
or an explicitly scoped binary-only contract backed by a new read-only host
query. Reopening by pathname, duplicating a descriptor to simulate adoption,
probing with writes, or accepting every integer would violate ownership,
position, access, or failure behavior.
The parent assignment explicitly allowed a design checkpoint if a coherent
implementation scope was not established. This is that checkpoint; _fdopen
remains unresolved pending the prerequisite and real I/O conformance tests.
The runtime diagnostic proved symbol lookup failure only, not that Mad Bomber
has yet invoked this API with any specific descriptor or mode.
Source findings
Audit evidence: scratch/runs/20261018-madbomber-fdopen-design/, containing
source-identity.json with seven physical source hashes, focused original
function excerpts, and the inspected upstream Wine source. Canonical source
was inspected after 7d31dba3c; the exact inspected HEAD is in the receipt.
| Existing path | Reusable behavior | Missing prerequisite or limit |
|---|---|---|
src/09a6-handlers-crt.wat, $handle__open |
Returns the VFS handle with requested read/write access. | Does not retain CRT open flags; _O_APPEND seeks only once. Text/binary state is not recorded. |
$handle__read, $handle__write, $handle__lseek |
Operate on that same handle and position. | No WAT descriptor table or metadata validation API. Existing byte passthrough is not text translation. |
$handle__dup |
Uses host_fs_duplicate_handle and maps errors. |
Does not propagate CRT text/binary/append metadata because none exists. |
$crt_fopen / $crt_file_init |
Owns a 48-byte record containing a real 32-byte guest FILE; _file is the VFS handle. |
Calls an opening function; cannot be reused wholesale for _fdopen without reopening/truncating. |
$crt_mode_flags |
Selects read/write/update stream flags. | Scans arbitrary trailing bytes and accepts unknown modifiers; not sufficient for requested strict mode validation. |
$crt_fclose, $crt_stream_close_all |
Unlinks/releases the FILE record and closes the stored handle. | Adoption must preserve that exact descriptor ownership and reject stale identities. |
$crt_freopen |
Closes the old descriptor and preserves the FILE pointer on successful replacement. | Any new descriptor metadata must retire the old association and initialize the new one too. |
lib/filesystem.js, getOpenFile / handle records |
Knows live/closed state, granted access and current position. | Those fields are not exposed to WAT as a read-only descriptor query. |
duplicateFileHandle |
Separate handle lifetimes share one position object; grants cannot escalate access. | Duplication would make fclose close the duplicate instead of the original passed to _fdopen. |
closeHandle |
Leaves a closed tombstone and clears pending reads; duplicate lifetimes remain separate. | Closing the original through a stream must preserve that behavior. |
fs_file_information |
Returns file identity, size, attributes and timestamps. | Does not expose granted read/write access or CRT modes. |
The existing VFS enforces data rights before reads/writes, including zero-byte operations. Nevertheless, using a trial read/write as an access query is inappropriate: the write path can update position for append-only handles and may interact with metadata/provider state. A harmless seek or file-size query establishes neither write permission nor CRT mode.
The FILE list is process-shared through CRT_STREAM_STATE, not a per-instance
WASM global. Any descriptor metadata must follow that process ownership or
the same main-host VFS instance used by Worker RPC. A private JavaScript map
on each Worker or a private mutable WAT global would break cross-thread use.
Contract references and unresolved historical detail
Microsoft's _fdopen documentation
states that the returned stream owns the supplied descriptor and that closing
the stream closes it. It describes read/write/update modes, append behavior,
text/binary translation, error returns and errno. These are useful contract
constraints; its modern invalid-parameter machinery is not automatically a
Win98 CRTDLL requirement.
Wine's CRT implementation
parses mode flags, allocates a FILE and initializes it against the supplied
descriptor, rather than opening the filename again. Its descriptor subsystem
already records open/text/append state. That architecture is evidence for
the missing prerequisite here, not code to copy or proof of exact vintage
CRTDLL behavior. Before setting a definitive compatibility policy, check
which mode modifiers alter the existing descriptor for the targeted CRTDLL;
modern documentation and Wine are not sufficient to assert all historical
edge cases. In particular, do not implement w by truncating an existing fd
or assume a is just a one-time seek.
Smallest coherent implementation proposal
- Add a read-only descriptor query at the main-host VFS boundary. Return explicit status plus live identity/generation, granted read/write rights, and retained CRT mode state. It must not read lazy file data, move the cursor, allocate a new descriptor, or change ownership. Preserve normal Worker RPC routing and regenerate/validate host-import signatures.
- Retain CRT open-mode state when
_opensucceeds; propagate it through_dup; retire it on_close,fclose,freopenand process cleanup. Metadata must distinguish a raw Win32 handle from a known CRT descriptor unless a reviewed bridge explicitly permits it. Define how preexistingfopenhandles and standard descriptors 0/1/2 participate; they cannot silently inherit arbitrary rights. Keep this state process-shared. - Establish the vintage mode grammar and a bounded parser. Reject NULL, malformed, conflicting or unsupported modes before acquiring ownership. A binary-first slice may be appropriate, but it must explicitly reject text/default-translation requests until that contract is implemented. A missing handler is preferable to silently treating text as binary.
- Validate the descriptor's live identity and access compatibility, reserve an ordinary FILE record without opening or seeking the underlying file, initialize its fields, then publish it through the existing shared list. Failed allocation/mode/access validation leaves the descriptor open and its position/bytes unchanged. No lock may span host RPC or heap allocation; revalidate identity before publication or use a small reservation protocol so a concurrent close cannot turn a validated descriptor into a stale one.
- Reads/writes/seeks through the FILE use the same handle and position.
fclosecloses precisely the adopted fd once, releases its record, and leaves distinct duplicated descriptors alive. Preserve caller-pop cdecl ABI and defined errno failures. Append at every write where required; access must never be promoted by the mode string. - Only once implemented, append
_fdopentoapi_table.json, generate both dispatch and API hash tables, and test dynamic resolution on the actual CRTDLL fallback route. Do not add a symbol-only stub to pass the packer.
This is more than one handler but remains a bounded CRT-descriptor change. It does not authorize broad text-I/O emulation, arbitrary guest-memory FILE adoption, or cross-CRT FILE sharing. If the slice is limited to binary owned descriptors, state and test those boundaries explicitly before integration.
Required real-I/O conformance
Extend the existing compiled test/test-crt-stdio-file.js style harness or
add a focused JavaScript test in the normal test tiers. Reuse real VFS, the
actual WAT handler and cdecl ESP assertions; do not substitute a fake FILE or
mock _fdopen return. test/test-duplicate-handle.js already exercises shared
cursor and independent close lifetimes and should remain a regression gate.
| Case | Observable requirement |
|---|---|
_open read/write binary, _lseek to 2, _fdopen("r+b") |
No extra VFS open, truncate or seek; FILE _file equals the original fd. |
Alternate _read and fread, then fseek and _lseek |
Byte data and one shared position advance consistently. |
fwrite through FILE, then read through descriptor |
Actual updated bytes, correct item count and position; existing content not truncated by adoption. |
| Write mode on read-only fd; read mode on write-only fd; update mode on either | Defined failure with no ownership/position/content change; no access escalation. |
| Append after seek to middle | Write occurs at end when required by chosen descriptor/mode contract, not just on initial adoption. |
fclose(FILE) |
Original fd closed, later descriptor I/O fails, FILE record retired; second close cannot close another file. |
_dup(fd) before adoption |
Shared position across both descriptors; closing the adopted fd leaves duplicate usable. |
| Failed mode, invalid/closed/foreign fd, OOM | NULL/defined errno; no descriptor leak, lost ownership, FILE list entry or data/position change. |
Premature _close(fd) followed by fclose(FILE) |
No stale-handle reuse or double-close of another descriptor; failure and record release are explicit. |
freopen and process cleanup after adoption |
Correct old/new descriptor lifetime and complete cleanup. |
| Process sharing and isolation | Same-process second instance sees ownership; independent process memory/VFS does not. |
| Dynamic GetProcAddress on CRTDLL | Nonzero handler address and real behavior; original shared MSVCRT export resolution still wins for its own image. |
Validation status: source audit and documentation checks only. No compiled conformance has been authored or executed for an implementation, because none is included. Next deliverable is the agreed descriptor metadata/query slice plus real read/write/seek/close tests; build/runtime requires the coordinator's separate resource grant.