Skip to content

Make the device load, run, and wire-respond on the Windows NT 3.1 floor - #22

Merged
connollydavid merged 7 commits into
mainfrom
nt31-pe-version-stamp
Jun 15, 2026
Merged

Make the device load, run, and wire-respond on the Windows NT 3.1 floor#22
connollydavid merged 7 commits into
mainfrom
nt31-pe-version-stamp

Conversation

@connollydavid

Copy link
Copy Markdown
Owner

Closes the #40 native-Win32 (NT 3.1) floor: the device now loads, runs, and
wire-responds on real Windows NT 3.1 build 511
over the OS-serial path
(on-target acceptance PASS).

Fixes

  • F1 — PE version stamp 3.10 (NT 3.1's loader rejects a 4.0 stamp).
  • F2SetHandleInformation routed through a feat.c runtime probe with a
    DuplicateHandle fallback (NT 3.1 lacks the API); fail-closed — the spawn
    aborts before CreateProcessA if a parent-only pipe end can't be made
    non-inheritable (weed gate-bypass finding).
  • F3lstrcpynAMcpStrCpyN (NT 3.1's kernel32 lacks lstrcpynA), a
    DBCS-aware bounded copy, swapped at ~50 sites.
  • COM1 openGetCommState-first DCB seeding (a from-zeroed DCB's
    XonChar==XoffChar was rejected by the 1993 serial driver with err 87);
    per-stage open-error reporting; \\.\COMn name fallback.

Gates

  • Allium lifecycle: tend (specs current, zero changes) → propagate (found 2
    test gaps) → implement (filled: theft_strutil 5 props ×50k + on-target;
    fail-closed handle-isolation tests) → weed (zero drift; tier gate holds).
  • CI: green (build-and-test + bridge + conformance + real-Windows).
  • Review gate: independent adversarial sub-agent — approve, zero findings.
  • Imports: {kernel32, user32}; allowlist reconciled (+DuplicateHandle
    +GetCommState, verified in the Win32s 1.25a W32SCOMB thunk).

🤖 Generated with Claude Code

connollydavid and others added 7 commits June 13, 2026 02:23
NT 3.1 (the native-Win32 floor, July 1993) rejected the binary at three
points the Win32s/Win9x tiers structurally never surfaced, each found by
on-target QEMU validation:

- The PE was stamped OS/subsystem version 4.0 (mingw default); NT 3.1's
  loader refuses a newer stamp with ERROR_BAD_FORMAT. Stamp 3.10 in the
  link step - still loads on every later Windows, which accept older stamps.
- SetHandleInformation (NT 3.51/Win95) was a static kernel32 import in
  exec_ops.c. Resolve it through the feat.c capability probe, with the
  classic DuplicateHandle non-inheritable-copy fallback for NT 3.1.
- lstrcpynA is absent from NT 3.1's kernel32 (only lstrcpy/A/W exist).
  Supply McpStrCpyN (strutil.c), a DBCS-aware bounded copy, and drop the
  static import across the call sites.

mcp-w32s.exe still imports only kernel32 + user32, stays i386/console, and
the full build plus every string-helper-affected suite is green (the two
exec/serial suites fail identically with and without this change - the known
wine-vs-native divergence, not a regression). Verified live: the device now
loads and runs on real NT 3.1 Advanced Server under QEMU. The remaining
serial-port-open issue is tracked separately in the host plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The NT 3.1 floor refuses CreateFileA on the bare "COMn" DOS-device alias
where later Windows resolves it; retry once via the canonical "\\.\COMn"
device-namespace form before failing (skipped if already prefixed). Also
surface the real CreateFileA GetLastError() + the port tried in the error
string, so an on-target open failure is diagnosable in a single boot
(error 2 => not enumerated; 5 => in use; 87 => bad name form).

No new import (wsprintfA is already user32); object stays opcode-clean.
Diagnostic+fallback build for the parked #40 COM1-open blocker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root-caused the NT 3.1 'failed to open serial port (error 87)': it was
SetCommState, not CreateFile. NT's MODE opens COM1 and accepts 19200/n/8/1
fine (it does GetCommState -> modify -> SetCommState), but the device built
its DCB from a memset(0) and called SetCommState cold -- a zeroed DCB has
XonChar == XoffChar == 0, which the 1993 serial driver rejects with
ERROR_INVALID_PARAMETER. Modern NT is lenient; NT 3.1 is not.

Fix: seed the DCB from GetCommState, then change only baud/bytesize/parity/
stop + the flow-control flags (the robust idiom MODE/Terminal use). Also
replace the mislabeled "CreateFileA error" string with the precise failing
stage (CreateFileA/GetCommState/SetCommState/SetCommTimeouts) + error code,
so one boot pinpoints the failure. \\.\COMn name fallback retained.

No new import (GetCommState is kernel32); object stays opcode-clean.
Pending on-target confirmation on the NT 3.1 guest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fallback

McpStrCpyN (the NT 3.1 bounded copy, used at ~50 sites) had no direct test.
Add a host theft PBT (tests/host/theft_strutil.c, 5 properties x 50k, ASan):
NUL-terminated & length <= n-1; result is a byte-prefix of src; no DBCS
character split (re-walk with a cp932-style CharNextA shim lands exactly on
the terminator); bounded write (dst sized exactly n, ASan red-zones catch
overflow); edges (dst==NULL, n<=0). Mirror on-target via prop.h
(tests/test_strutil.c).

F2's ClearHandleInherit DuplicateHandle fallback (taken only when NT 3.1
lacks SetHandleInformation) was untested. Expose a TEST_BUILD hook and add
test_exec_ops cases: force the probe NULL, confirm GetHandleInformation
shows HANDLE_FLAG_INHERIT cleared on a still-valid handle; the API path; the
NULL/invalid guards.

Also fix a pre-existing host-pbt breakage: the theft_catalog build line
lacked src/strutil.c, so catalog.c's McpStrCpyN calls were undefined refs
(broken since the F3 swap) -- host-pbt was failing in CI on this branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The weed gate-bypass audit found ClearHandleInherit fails open: it dropped
HANDLE_FLAG_INHERIT on the parent-only pipe ends but ignored failure on both
routes (SetHandleInformation return discarded; DuplicateHandle fallback left
the original inheritable handle on failure), so a child could inherit a
parent-only pipe end. Make it return BOOL and abort the spawn (goto fail_pipes,
before CreateProcessA) if any clear fails, rather than launching with an
inheritable parent end. Never-path on a fresh handle, benign blast radius, but
fail-closed matches the project's restraint posture.

Tests: assert the success routes return TRUE; a stubbed SetHandleInformation
returning FALSE is now DETECTED (failclosed_unit) and aborts ExecOpRun with
"could not isolate parent pipe handles" before any child spawn (failclosed_spawn).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI's function-level allowlist check has been red since the NT-floor work
because the import set drifted: F2 made SetHandleInformation a GetProcAddress
probe (no longer a static import) and added DuplicateHandle (fallback); F3
dropped lstrcpynA for McpStrCpyN; the COM1 fix added GetCommState. Update the
allowlist to match exactly: +DuplicateHandle +GetCommState, -SetHandleInformation
-lstrcpynA. Both additions verified present in the Win32s 1.25a thunk export
surface (W32SCOMB.DLL exports GetCommState[697] and DuplicateHandle[2015],
alongside the already-listed SetCommState/SetCommTimeouts), per the file header's
baseline-image method. Local replication of the CI check: 58 imports, zero drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The \\.\COMn fallback used an unbounded wsprintfA into devName[40], safe only
by the implicit port[32] cross-module bound. Build it explicitly bounded
instead: lstrcpyA the fixed 4-char prefix, then McpStrCpyN the port name into
the remainder (sizeof(devName)-4), independent of the caller's buffer size.
No import change (lstrcpyA/McpStrCpyN already used); allowlist still exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@connollydavid
connollydavid merged commit ec15294 into main Jun 15, 2026
8 checks passed
@connollydavid
connollydavid deleted the nt31-pe-version-stamp branch June 15, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant