Skip to content
42 changes: 31 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,19 @@ endif()
# Win32s load requirements: the PE must carry relocations (/FIXED:NO) and be
# based at 0x10000 (the default 0x400000 region is occupied by Win32s).
# Applied to every executable we emit.
#
# PE version stamp: the native-Win32 floor is Windows NT 3.1 (1993), whose loader
# rejects a PE stamped with a newer OS/subsystem version (ERROR_BAD_FORMAT) - and
# mingw/MSVC default the stamp to 4.0. Stamp 3.10 so the 1993 loader accepts it;
# 3.10 still loads on every later Windows (they accept older stamps). The Win32s/
# Win9x loaders do not enforce this, so only NT 3.1 surfaced it (see the host
# repo's plan/PHASE6.md, NT 3.1 floor validation).
if(MSVC)
add_link_options(/FIXED:NO /BASE:0x10000 /MACHINE:I386 /SUBSYSTEM:CONSOLE)
add_link_options(/FIXED:NO /BASE:0x10000 /MACHINE:I386 /SUBSYSTEM:CONSOLE,3.10)
else()
add_link_options(-Wl,--dynamicbase -Wl,--image-base,0x10000)
add_link_options(-Wl,--dynamicbase -Wl,--image-base,0x10000
-Wl,--major-os-version,3 -Wl,--minor-os-version,10
-Wl,--major-subsystem-version,3 -Wl,--minor-subsystem-version,10)
endif()

include_directories(src tests)
Expand All @@ -96,7 +105,8 @@ set(CORE_SOURCES
src/pty_exec.c
src/toolchain_probe.c
src/mem_ops.c
src/audit.c)
src/audit.c
src/strutil.c)

# --- main executable -------------------------------------------------------
# The TCP backend is runtime-loaded (LoadLibraryA), so we deliberately do NOT
Expand Down Expand Up @@ -142,7 +152,7 @@ target_link_libraries(test_transport PRIVATE kernel32)

# test_tcp's own client side links wsock32 directly; the backend under test
# still resolves Winsock at runtime.
add_executable(test_tcp tests/test_tcp.c src/tcp.c src/transport.c)
add_executable(test_tcp tests/test_tcp.c src/tcp.c src/transport.c src/strutil.c)
target_link_libraries(test_tcp PRIVATE kernel32 wsock32)

# test_serial includes mcp-w32s.c with TEST_BUILD to exclude main() and reach
Expand All @@ -166,6 +176,13 @@ target_link_libraries(test_file_ops PRIVATE kernel32 user32)
add_executable(test_pbt_base64 tests/test_pbt_base64.c src/base64.c)
target_link_libraries(test_pbt_base64 PRIVATE kernel32)

# test_strutil: the prop.h on-target mirror of the McpStrCpyN properties
# (bounded/NUL/prefix/no-split), proving the device's own DBCS-aware bounded
# copy holds on the shipped C89/i386 path. The deep 50k-trial theft host run
# lives in tests/host/theft_strutil.c (built by build.sh host-pbt).
add_executable(test_strutil tests/test_strutil.c src/strutil.c)
target_link_libraries(test_strutil PRIVATE kernel32 user32)

# --- Phase 4 test targets ---------------------------------------------------
add_executable(test_feat tests/test_feat.c src/feat.c)
target_link_libraries(test_feat PRIVATE kernel32)
Expand All @@ -182,14 +199,17 @@ add_executable(test_binfmt tests/test_binfmt.c src/binfmt.c src/feat.c)
target_link_libraries(test_binfmt PRIVATE kernel32)

# catalog.c serialises the listCommands listing via json_parser.c's JsonEscape.
add_executable(test_catalog tests/test_catalog.c src/catalog.c src/json_parser.c)
add_executable(test_catalog tests/test_catalog.c src/catalog.c src/json_parser.c src/strutil.c)
target_link_libraries(test_catalog PRIVATE kernel32)

# exec_ops.c widens the cmdline for the -W spawn (encoding.c), so the
# standalone exec target links the codec + tables too.
add_executable(test_exec_ops
tests/test_exec_ops.c src/exec_ops.c src/feat.c src/binfmt.c
src/encoding.c src/charset_tables.c src/charset_tables_data.c)
src/encoding.c src/charset_tables.c src/charset_tables_data.c src/strutil.c)
# TEST_BUILD exposes ExecClearHandleInheritForTest (the static
# ClearHandleInherit hook) so the NT 3.1 DuplicateHandle fallback is testable.
target_compile_definitions(test_exec_ops PRIVATE TEST_BUILD)
target_link_libraries(test_exec_ops PRIVATE kernel32)

add_executable(test_pty_exec tests/test_pty_exec.c src/pty_exec.c src/feat.c)
Expand All @@ -199,7 +219,7 @@ target_link_libraries(test_pty_exec PRIVATE kernel32)
add_executable(test_toolchain_probe
tests/test_toolchain_probe.c src/toolchain_probe.c
src/exec_ops.c src/catalog.c src/json_parser.c src/feat.c src/binfmt.c
src/encoding.c src/charset_tables.c src/charset_tables_data.c)
src/encoding.c src/charset_tables.c src/charset_tables_data.c src/strutil.c)
target_link_libraries(test_toolchain_probe PRIVATE kernel32)

# --- 5.3 memory peek/poke test targets -------------------------------------
Expand All @@ -208,7 +228,7 @@ target_link_libraries(test_toolchain_probe PRIVATE kernel32)
# helper child next to it. audit.c is linked because MemPoke audits.
add_executable(test_mem_ops
tests/test_mem_ops.c src/mem_ops.c src/audit.c
src/feat.c src/catalog.c src/json_parser.c src/argv.c)
src/feat.c src/catalog.c src/json_parser.c src/argv.c src/strutil.c)
target_compile_definitions(test_mem_ops PRIVATE TEST_BUILD)
target_link_libraries(test_mem_ops PRIVATE kernel32 user32)

Expand All @@ -217,7 +237,7 @@ add_executable(mem_target tests/mem_target.c)
target_link_libraries(mem_target PRIVATE kernel32)
add_dependencies(test_mem_ops mem_target)

add_executable(test_audit tests/test_audit.c src/audit.c)
add_executable(test_audit tests/test_audit.c src/audit.c src/strutil.c)
target_link_libraries(test_audit PRIVATE kernel32 user32)

# --- 5.4 text-encoding test targets ----------------------------------------
Expand All @@ -242,7 +262,7 @@ target_link_libraries(test_pbt_encoding PRIVATE kernel32)
# test_uart: the prop.h on-target mirror of the theft UART properties, proving
# the pure detection ladder + driving logic hold on the shipped C89/i386 path.
# UART_HOST_PURE excludes the asm IN/OUT seam + transport wiring (links uart.c only).
add_executable(test_uart tests/test_uart.c src/uart.c)
add_executable(test_uart tests/test_uart.c src/uart.c src/strutil.c)
target_compile_definitions(test_uart PRIVATE UART_HOST_PURE)
target_link_libraries(test_uart PRIVATE kernel32)

Expand All @@ -267,7 +287,7 @@ add_custom_command(TARGET test_catalog POST_BUILD
# CMAKE_CROSSCOMPILING_EMULATOR (set by the toolchain: empty for native
# WSL-interop execution, "wine" otherwise) is applied to each test command.
foreach(t test_json test_transport test_tcp test_serial
test_base64 test_file_ops test_pbt_base64
test_base64 test_file_ops test_pbt_base64 test_strutil
test_feat test_argv test_binfmt test_catalog
test_exec_ops test_pty_exec test_toolchain_probe
test_mem_ops test_audit test_encoding test_pbt_encoding test_uart)
Expand Down
7 changes: 6 additions & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ if [ "$1" = "host-pbt" ]; then
gcc $HFLAGS tests/host/theft_base64.c src/base64.c build/host/theft/*.o -lm -o build/host/theft_base64
gcc $HFLAGS tests/host/theft_json.c src/json_parser.c build/host/theft/*.o -lm -o build/host/theft_json
gcc $HFLAGS tests/host/theft_argv.c src/argv.c build/host/theft/*.o -lm -o build/host/theft_argv
gcc $HFLAGS tests/host/theft_catalog.c src/catalog.c src/json_parser.c build/host/theft/*.o -lm -o build/host/theft_catalog
gcc $HFLAGS tests/host/theft_catalog.c src/catalog.c src/json_parser.c src/strutil.c build/host/theft/*.o -lm -o build/host/theft_catalog
# strutil: the DBCS-aware bounded copy (McpStrCpyN). CharNextA resolves to
# the cp932-style shim (tests/host/win32_shim.h) so the no-split property
# is deterministically testable - the bounded/NUL/prefix/no-split pins at 50k.
gcc $HFLAGS tests/host/theft_strutil.c src/strutil.c build/host/theft/*.o -lm -o build/host/theft_strutil
# mem_ops: only the two pure arithmetic guards compile natively
# (MEM_OPS_HOST_PURE excludes the Win32 surface) - the off-by-overflow pin.
gcc $HFLAGS -DMEM_OPS_HOST_PURE tests/host/theft_mem.c src/mem_ops.c build/host/theft/*.o -lm -o build/host/theft_mem
Expand All @@ -45,6 +49,7 @@ if [ "$1" = "host-pbt" ]; then
build/host/theft_json
build/host/theft_argv
build/host/theft_catalog
build/host/theft_strutil
build/host/theft_mem
build/host/theft_encoding
build/host/theft_uart
Expand Down
3 changes: 2 additions & 1 deletion src/audit.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

#include <windows.h>
#include "strutil.h"
#include "audit.h"

#define AUDIT_DEFAULT_NAME "audit-mem.log"
Expand Down Expand Up @@ -78,7 +79,7 @@ int AuditIsWritable(void)
int AuditConfigure(int armRequested, const char *path)
{
if (path != NULL && path[0] != '\0') {
lstrcpynA(g_auditPath, path, (int)sizeof(g_auditPath));
McpStrCpyN(g_auditPath, path, (int)sizeof(g_auditPath));
} else {
resolve_default_path(g_auditPath, (int)sizeof(g_auditPath));
}
Expand Down
17 changes: 9 additions & 8 deletions src/catalog.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

#include <windows.h>
#include "strutil.h"
#include <stdlib.h>
#include <string.h>
#include "catalog.h"
Expand Down Expand Up @@ -57,7 +58,7 @@ typedef struct {
static void scanError(Scanner *s, const char *msg)
{
if (s->err != NULL && s->errSize > 0) {
lstrcpynA(s->err, msg, s->errSize);
McpStrCpyN(s->err, msg, s->errSize);
}
}

Expand Down Expand Up @@ -448,7 +449,7 @@ static int parseCommands(Scanner *s, Catalog *cat)
s->p++;
if (cat->entry_count < CATALOG_MAX_ENTRIES) {
CatalogEntry *e = &cat->entries[cat->entry_count];
lstrcpynA(e->name, name, sizeof(e->name));
McpStrCpyN(e->name, name, sizeof(e->name));
if (!parseEntry(s, e)) {
return 0;
}
Expand Down Expand Up @@ -573,7 +574,7 @@ int CatalogLoad(const char *path, Catalog **outCat, char *errMsg, int errSize)
fileBuf = (char *)malloc(CATALOG_FILE_MAX);
if (fileBuf == NULL) {
if (errMsg != NULL) {
lstrcpynA(errMsg, "out of memory", errSize);
McpStrCpyN(errMsg, "out of memory", errSize);
}
return 0;
}
Expand All @@ -582,7 +583,7 @@ int CatalogLoad(const char *path, Catalog **outCat, char *errMsg, int errSize)
if (len < 0) {
free(fileBuf);
if (errMsg != NULL) {
lstrcpynA(errMsg, "catalog file not found or unreadable", errSize);
McpStrCpyN(errMsg, "catalog file not found or unreadable", errSize);
}
return 0;
}
Expand All @@ -591,7 +592,7 @@ int CatalogLoad(const char *path, Catalog **outCat, char *errMsg, int errSize)
if (cat == NULL) {
free(fileBuf);
if (errMsg != NULL) {
lstrcpynA(errMsg, "out of memory", errSize);
McpStrCpyN(errMsg, "out of memory", errSize);
}
return 0;
}
Expand All @@ -615,7 +616,7 @@ int CatalogLoad(const char *path, Catalog **outCat, char *errMsg, int errSize)
free(fileBuf);
free(cat);
if (errMsg != NULL) {
lstrcpynA(errMsg, "no commands in catalog", errSize);
McpStrCpyN(errMsg, "no commands in catalog", errSize);
}
return 0;
}
Expand Down Expand Up @@ -702,7 +703,7 @@ int CatalogValidateArgs(const CatalogEntry *entry, const char **argv,
}
if (entry == NULL) {
if (errMsg != NULL) {
lstrcpynA(errMsg, "no catalog entry", errSize);
McpStrCpyN(errMsg, "no catalog entry", errSize);
}
return 0;
}
Expand All @@ -717,7 +718,7 @@ int CatalogValidateArgs(const CatalogEntry *entry, const char **argv,
const CatalogOption *opt = findOption(entry, tok);
if (opt == NULL) {
if (errMsg != NULL) {
lstrcpynA(errMsg, "argument not allowed", errSize);
McpStrCpyN(errMsg, "argument not allowed", errSize);
}
return 0;
}
Expand Down
58 changes: 53 additions & 5 deletions src/exec_ops.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "feat.h"
#include "binfmt.h"
#include "encoding.h" /* Utf8ToUtf16 for the -W spawn (wide tier) */
#include "strutil.h" /* McpStrCpyN (the NT 3.1 floor lacks lstrcpynA) */

/* ------------------------------------------------------------------
* Job-object declarations. MinGW's C89 headers may lack these; declare
Expand Down Expand Up @@ -124,10 +125,52 @@ typedef struct {
static void SetMsg(char *errMsg, int errSize, const char *s)
{
if (errMsg != NULL && errSize > 0) {
lstrcpynA(errMsg, s, errSize);
McpStrCpyN(errMsg, s, errSize);
}
}

/*
* ClearHandleInherit - clear a handle's HANDLE_FLAG_INHERIT (Q5: the
* parent-only pipe ends must not be inherited by the child). NT 3.1's kernel32
* - the native-Win32 floor - has no SetHandleInformation (it arrived in NT
* 3.51/Win95), so when the runtime probe found it absent we fall back to the
* classic pre-3.51 idiom: duplicate the handle non-inheritable and drop the
* inheritable original. DuplicateHandle is present since NT 3.1.
*
* Fails closed: returns TRUE iff the handle is guaranteed non-inheritable
* afterward. A discarded SetHandleInformation/DuplicateHandle failure would
* leave an inheritable parent end the child could leak, so the caller aborts
* the spawn when this returns FALSE.
*/
static BOOL ClearHandleInherit(HANDLE *ph)
{
HANDLE dup;
if (ph == NULL || *ph == NULL || *ph == INVALID_HANDLE_VALUE) {
return TRUE;
}
if (g_features.pSetHandleInformation != NULL) {
return g_features.pSetHandleInformation(*ph, HANDLE_FLAG_INHERIT, 0);
}
if (DuplicateHandle(GetCurrentProcess(), *ph,
GetCurrentProcess(), &dup,
0, FALSE, DUPLICATE_SAME_ACCESS)) {
CloseHandle(*ph);
*ph = dup;
return TRUE;
}
return FALSE;
}

#ifdef TEST_BUILD
/* Test-only hook: invoke the static ClearHandleInherit so test_exec_ops can
* pin both the SetHandleInformation route and the NT 3.1 DuplicateHandle
* fallback (forced by NULLing g_features.pSetHandleInformation). */
BOOL ExecClearHandleInheritForTest(HANDLE *ph)
{
return ClearHandleInherit(ph);
}
#endif

/*
* PumpPipe - polling-path non-blocking drain (Q3). PeekNamedPipe to find
* how many bytes are available; ReadFile up to the smaller of that and
Expand Down Expand Up @@ -374,10 +417,15 @@ int ExecOpRun(
SetMsg(errMsg, errSize, "pipe creation failed");
goto fail_pipes;
}
/* Q5: child must not inherit the parent-only ends. */
SetHandleInformation(inWr, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(outRd, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(errRd, HANDLE_FLAG_INHERIT, 0);
/* Q5: child must not inherit the parent-only ends. Fail closed - if the
* inherit flag cannot be dropped, do NOT spawn with an inheritable parent
* end (a child handle leak); abort the open instead. */
if (!ClearHandleInherit(&inWr) ||
!ClearHandleInherit(&outRd) ||
!ClearHandleInherit(&errRd)) {
SetMsg(errMsg, errSize, "spawn failed: could not isolate parent pipe handles");
goto fail_pipes;
}

memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
Expand Down
12 changes: 12 additions & 0 deletions src/exec_ops.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,16 @@ int ExecOpRun(
char *errMsg, int errSize
);

#ifdef TEST_BUILD
/*
* ExecClearHandleInheritForTest - test-only hook onto the static
* ClearHandleInherit, so test_exec_ops can pin BOTH routes: the
* SetHandleInformation path and the NT 3.1 DuplicateHandle fallback (reached
* by forcing g_features.pSetHandleInformation to NULL in the test).
* Returns TRUE iff the handle is guaranteed non-inheritable afterward
* (the helper fails closed).
*/
BOOL ExecClearHandleInheritForTest(HANDLE *ph);
#endif

#endif /* EXEC_OPS_H */
7 changes: 7 additions & 0 deletions src/feat.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ void FeatInit(void)
g_features.has_get_binary_type = 1;
}

/* Absent on the NT 3.1 floor; callers fall back to DuplicateHandle. */
proc = GetProcAddress(hKernel, "SetHandleInformation");
if (proc != NULL) {
g_features.pSetHandleInformation =
(BOOL (WINAPI *)(HANDLE, DWORD, DWORD))proc;
}

proc = GetProcAddress(hKernel, "IsWow64Process");
if (proc != NULL) {
g_features.pIsWow64Process =
Expand Down
2 changes: 2 additions & 0 deletions src/feat.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ typedef struct {
BOOL (WINAPI *pAssignProcessToJobObject)(HANDLE, HANDLE);
BOOL (WINAPI *pSetInformationJobObject)(HANDLE, int, LPVOID, DWORD);
BOOL (WINAPI *pGetBinaryTypeA)(LPCSTR, LPDWORD);
/* NT 3.1's kernel32 lacks SetHandleInformation (arrived NT 3.51/Win95). */
BOOL (WINAPI *pSetHandleInformation)(HANDLE, DWORD, DWORD);
BOOL (WINAPI *pIsWow64Process)(HANDLE, BOOL *);
BOOL (WINAPI *pGenerateConsoleCtrlEvent)(DWORD, DWORD);
BOOL (WINAPI *pQueryFullProcessImageNameA)(HANDLE, DWORD, LPSTR, LPDWORD);
Expand Down
Loading
Loading