From 8e776aa9a9a71c1da5550196bd8fa2b1c68f9e45 Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 12:45:25 -0700 Subject: [PATCH 1/8] Skip empty unit table slots in C_SystemTask and C_SystemMenu Both loops walk LM(UTableBase)[0..UnitNtryCnt) and dereference each entry without checking it. The unit table is sparse -- native drivers install themselves at fixed unit numbers, leaving the slots between them null -- so any empty slot below the loop bound is a null deref. This is latent rather than live: LM(UnitNtryCnt) is set to 0 in init.cpp and never updated by anything, so neither loop body has ever executed and no driver has ever been sent accRun. How UnitNtryCnt ought to be maintained is a separate Device Manager question; guard the dereference first so that raising it is safe. --- src/desk.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/desk.cpp b/src/desk.cpp index d9b3d559..073cd032 100644 --- a/src/desk.cpp +++ b/src/desk.cpp @@ -164,6 +164,17 @@ void Executor::C_SystemTask() for(i = 0; i < LM(UnitNtryCnt); ++i) { dctlh = LM(UTableBase)[i]; + /* The unit table is sparse: native drivers install themselves at + * fixed unit numbers (serial at 5..8) and leave the slots in + * between empty. Skip empty slots rather than dereference them. + * + * NOTE: LM(UnitNtryCnt) is set to 0 in init.cpp and never + * updated, so this loop never actually runs -- meaning accRun is + * never delivered to any driver. Fixing that is a Device + * Manager design question; this guard is what makes raising + * UnitNtryCnt safe when someone does. */ + if(!dctlh || !*dctlh) + continue; if(((*dctlh)->dCtlFlags & NEEDTIMEBIT) && TickCount() >= (*dctlh)->dCtlCurTicks) { Control(itorn(i), accRun, (Ptr)0); @@ -241,6 +252,8 @@ void Executor::C_SystemMenu(LONGINT menu) for(i = 0; i < LM(UnitNtryCnt); ++i) { dctlh = LM(UTableBase)[i]; + if(!dctlh || !*dctlh) /* sparse unit table -- see C_SystemTask */ + continue; if((*dctlh)->dCtlMenu == LM(MBarEnable)) { menu_s = menu; From 311794338324453f8522b9a2a567750037c0ebac Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 12:45:42 -0700 Subject: [PATCH 2/8] Move callcomp out of serial.cpp so other drivers can use it callcomp re-enters emulated code to run a driver call's completion routine (A0 = param block, A1 = the routine, D0 = result). It was defined in serial.cpp at global scope, where no other translation unit could reach it, even though it implements a Device Manager convention rather than anything serial-specific. Move the definition to device.cpp and declare it in rsys/device.h. No behaviour change; serial.cpp picks it up via using namespace Executor. --- src/device.cpp | 11 +++++++++++ src/include/rsys/device.h | 6 ++++++ src/serial.cpp | 12 +++--------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/device.cpp b/src/device.cpp index 03504aac..408d0db2 100644 --- a/src/device.cpp +++ b/src/device.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include using namespace Executor; @@ -258,6 +259,16 @@ void Executor::RegisterDriver(const driverinfo& di) knowndrivers.push_back(di); } +/* Re-enter emulated code to run a driver call's completion routine. + * A0 = param block, A1 = the routine, D0 = result. */ +void Executor::callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err) +{ + EM_A0 = US_TO_SYN68K(pbp); + EM_A1 = US_TO_SYN68K(comp); + EM_D0 = (unsigned short)err; /* TODO: unsigned short ? */ + execute68K((syn68k_addr_t)(uintptr_t)comp); +} + static void InitBuiltinDrivers() { InitSerialDriver(); diff --git a/src/include/rsys/device.h b/src/include/rsys/device.h index 91b27e99..42c1938d 100644 --- a/src/include/rsys/device.h +++ b/src/include/rsys/device.h @@ -17,4 +17,10 @@ struct driverinfo }; void RegisterDriver(const driverinfo& di); + +/* Re-enter emulated code to run a driver call's completion routine. + * A0 = param block, A1 = the routine, D0 = result -- the completion + * routine ABI. Any native driver implementing asynchronous calls + * needs this, so it lives here rather than in one driver's .cpp. */ +void callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err); } diff --git a/src/serial.cpp b/src/serial.cpp index 3a3b70cc..7af1ac59 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -286,16 +286,10 @@ static const char *specialname(ParmBlkPtr pbp) typedef void (*compfuncp)(void); +/* callcomp() now lives in device.cpp and is declared in rsys/device.h, + * so that other native drivers can complete asynchronous calls too. */ -void callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err) -{ - EM_A0 = US_TO_SYN68K(pbp); - EM_A1 = US_TO_SYN68K(comp); - EM_D0 = (unsigned short)err; /* TODO: unsigned short ? */ - execute68K((syn68k_addr_t)(uintptr_t)comp); -} - -#define DOCOMPLETION(pbp, err) \ +#define DOCOMPLETION(pbp, err) \ (pbp)->ioParam.ioResult = err; \ if(((pbp)->ioParam.ioTrap & asyncTrpBit) && (pbp)->ioParam.ioCompletion) \ callcomp(pbp, (pbp)->ioParam.ioCompletion, err); \ From 39aeb20a620c7351683216a9e845595aa55511d7 Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 12:46:26 -0700 Subject: [PATCH 3/8] Add a native MacTCP (.IPP) driver MacTCP is reached by applications through PBOpen("\p.IPP") plus PBControl with a csCode, so it needs no trap work at all: the whole feature is a device driver plus the type definitions that just landed in multiversal. Phase 1, modelled on serial.cpp: driver registration at .IPP / refnum -48, csCode dispatch, an opaque stream-cookie table, and synchronous TCP client operations over non-blocking POSIX sockets -- ipctlGetAddr, TCPCreate, TCPActiveOpen, TCPSend (writev over the WDS gather list), TCPRcv, TCPClose (half-close), TCPAbort (RST via SO_LINGER 0), TCPStatus and TCPRelease. Guest memory is big-endian, so htonl/htons at the sockaddr boundary is both necessary and sufficient. Operations execute synchronously and then complete, the same idiom serial.cpp uses. Phase 2 replaces the bodies with a pending-op queue drained from an accRun pump -- which will first require sorting out LM(UnitNtryCnt), since accRun is currently never delivered to anyone. Passive open, NoCopyRcv, UDP, the ASR and the DNR are all stubbed with notes rather than half-implemented. The documented parameter block offsets are asserted at build time, so a disagreement between the generated headers and the MacTCP ABI fails the build instead of corrupting guest memory at run time. Also bumps the multiversal submodule, which carries the MacTCP definitions along with the two generator fixes on that branch. --- multiversal | 2 +- src/CMakeLists.txt | 8 + src/device.cpp | 2 + src/include/rsys/mactcp.h | 11 + src/mactcp/mactcp.cpp | 696 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 src/include/rsys/mactcp.h create mode 100644 src/mactcp/mactcp.cpp diff --git a/multiversal b/multiversal index 05cdec2f..53efee90 160000 --- a/multiversal +++ b/multiversal @@ -1 +1 @@ -Subproject commit 05cdec2fa261b21712597549d08481b79b7b1d9b +Subproject commit 53efee90c122ae8e8cd9158a89925141a25c724c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 12f0b96c..47ce764e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -466,6 +466,12 @@ set(prefs_sources source_group(Prefs FILES ${prefs_sources}) +set(mactcp_sources + mactcp/mactcp.cpp +) + +source_group(MacTCP FILES ${mactcp_sources}) + set(util_sources util/macstrings.h util/macstrings.cpp @@ -536,6 +542,7 @@ set(include_sources include/rsys/icon.h include/rsys/keyboard.h include/rsys/launch.h + include/rsys/mactcp.h include/rsys/macros.h include/rsys/noreturn.h include/rsys/osutil.h @@ -567,6 +574,7 @@ set(sources ${base_sources} ${mman_sources} ${vdriver_sources} ${sound_sources} ${num_sources} ${misc_sources} ${file_sources} ${hfs_sources} ${time_sources} ${osevent_sources} ${error_sources} ${commandline_sources} ${prefs_sources} + ${mactcp_sources} ${util_sources} ${debug_sources} ${api_headers} ${trap_instance_sources} ${host_os_sources} ${mpw_sources} diff --git a/src/device.cpp b/src/device.cpp index 408d0db2..3d8e6b46 100644 --- a/src/device.cpp +++ b/src/device.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -272,6 +273,7 @@ void Executor::callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err) static void InitBuiltinDrivers() { InitSerialDriver(); + InitMacTCPDriver(); } /* diff --git a/src/include/rsys/mactcp.h b/src/include/rsys/mactcp.h new file mode 100644 index 00000000..d0fc848c --- /dev/null +++ b/src/include/rsys/mactcp.h @@ -0,0 +1,11 @@ +/* + * rsys/mactcp.h — native MacTCP (.IPP) driver for Executor 2000. + */ +#if !defined(_RSYS_MACTCP_H_) +#define _RSYS_MACTCP_H_ + +namespace Executor +{ +void InitMacTCPDriver(); +} +#endif diff --git a/src/mactcp/mactcp.cpp b/src/mactcp/mactcp.cpp new file mode 100644 index 00000000..6be49a2e --- /dev/null +++ b/src/mactcp/mactcp.cpp @@ -0,0 +1,696 @@ +/* mactcp.cpp — native `.IPP` (MacTCP) driver for Executor 2000. + * + * Phase 1 skeleton: driver registration, csCode dispatch, stream + * table, and synchronous TCP client operations over non-blocking + * POSIX sockets. Modeled on src/serial.cpp. + * + * Design notes: + * - Async model: like serial.cpp, operations currently execute + * synchronously and then "complete" (set ioResult, invoke the + * ioCompletion routine via callcomp if the async trap bit is + * set). Phase 2 replaces the bodies with a pending-op queue + * drained from the accRun pump. + * - Pump: driver open sets NEEDTIMEBIT/dCtlDelay=0 on the DCE, which + * is what C_SystemTask() (desk.cpp) looks for when deciding whom to + * send accRun to. + * + * BLOCKER FOR PHASE 2: that pump does not currently run at all. + * LM(UnitNtryCnt) is set to 0 in init.cpp and is never updated by + * anything, so C_SystemTask's loop body never executes and no + * driver has ever received accRun. Raising it naively would then + * null-deref on the empty slots between installed units, so + * C_SystemTask and C_SystemMenu now skip empty slots. Deciding + * how UnitNtryCnt should actually be maintained is a Device + * Manager question to settle with upstream before phase 2 depends + * on it; phase 1 is unaffected because it is fully synchronous. + * - Apps that spin-poll ioResult without calling WaitNextEvent will + * starve even once the pump works; a secondary pump point in the + * trap path may be needed. + * - StreamPtr: an opaque cookie (never a real pointer); apps are + * documented to treat it as opaque. + * - Byte order: guest is big-endian == network order; GUEST<> + * reads yield host-order values, so htonl/htons at the sockaddr + * boundary is both necessary and sufficient. + */ + +#include +#include +#include +#include +#include +#include /* accRun */ +#include /* generated from defs/MacTCP.yaml */ +#include +#include +/* Required for REGISTER_FUNCTION_PTR below: it expands to an explicit + * template instantiation of WrappedFunction<>, whose member definitions + * live here. Without it everything compiles and the link fails on + * WrappedFunction<...>::init(). serial.cpp includes it for the same + * reason. */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* writev, struct iovec */ +#include + +#include +#include +#include + +using namespace Executor; + +/* ---- ABI guard rails ------------------------------------------------ + * If the generated headers disagree with the documented MacTCP ABI, + * fail the build rather than corrupt guest memory at run time. + */ +static_assert(offsetof(TCPiopb, ioCompletion) == 12, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, ioResult) == 16, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, ioCRefNum) == 24, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, csCode) == 26, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, tcpStream) == 28, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, csParam) == 32, "TCPiopb ABI"); +static_assert(offsetof(TCPOpenPB, remoteHost) == 4, "TCPOpenPB ABI"); +static_assert(offsetof(TCPOpenPB, localHost) == 10, "TCPOpenPB ABI"); +static_assert(offsetof(TCPSendPB, wdsPtr) == 6, "TCPSendPB ABI"); +static_assert(offsetof(TCPReceivePB, rcvBuff) == 2, "TCPReceivePB ABI"); +static_assert(offsetof(GetAddrParamBlock, ourAddress) == 28, + "GetAddrParamBlock ABI"); +static_assert(sizeof(wdsEntry) == 6, "wdsEntry ABI"); + +/* callcomp() (re-enter emulated code to run a completion routine) is + * declared in rsys/device.h and defined in device.cpp. */ + +namespace +{ + +/* ---- Stream table -------------------------------------------------- */ + +enum class StreamState : uint8_t +{ + created, /* TCPCreate done, no connection yet */ + connecting, /* reserved for phase 2 async open */ + established, + closing, /* we sent FIN (TCPClose) */ + terminated, /* connection gone, stream not yet released */ +}; + +struct MacTCPStream +{ + int fd = -1; + StreamState state = StreamState::created; + bool remoteClosed = false; /* peer FIN seen */ + + /* App-supplied receive area (guest memory), from TCPCreate. + * Phase 1 (TCPRcv only) copies straight into the caller's + * buffer; phase 2's TCPNoCopyRcv stages data here and hands + * out rdsEntry pointers into it. */ + Ptr rcvBuff = nullptr; + uint32_t rcvBuffLen = 0; + + /* ASR + per-stream user data, from TCPCreate. */ + ProcPtr notifyProc = nullptr; + Ptr userDataPtr = nullptr; + + /* TODO(phase2): pending async op queue; rds bookkeeping. */ +}; + +/* StreamPtr cookies: 'TCP\0' | id. Never dereferenced. */ +constexpr uint32_t STREAM_COOKIE_BASE = 0x54435000; +uint32_t next_stream_id = 1; +std::unordered_map streams; + +MacTCPStream *lookup(GUEST sp, OSErr *err) +{ + auto it = streams.find(guest_cast(sp)); + if(it == streams.end()) + { + *err = invalidStreamPtr; + return nullptr; + } + return &it->second; +} + +/* ---- errno -> MacTCP OSErr ---------------------------------------- */ + +OSErr map_socket_errno(int e) +{ + switch(e) + { + case 0: + return noErr; + case ECONNREFUSED: + case EHOSTUNREACH: + case ENETUNREACH: + return openFailed; + case ETIMEDOUT: + return commandTimeout; + case ECONNRESET: + case EPIPE: + return connectionTerminated; + case EADDRINUSE: + return duplicateSocket; + case ENOBUFS: + case ENOMEM: + case EMFILE: + case ENFILE: + return insufficientResources; + default: + return ipBadAddr; /* generic; refine as cases surface */ + } +} + +/* ---- completion ---------------------------------------------------- */ + +OSErr complete(TCPiopb *pb, OSErr err) +{ + pb->ioResult = err; + if((pb->ioTrap & asyncTrpBit) && pb->ioCompletion) + callcomp(guest_cast(pb), pb->ioCompletion, err); + return err; +} + +/* ---- ASR delivery (phase 2) --------------------------------------- + * pascal void notifyProc(StreamPtr, u16 eventCode, Ptr userDataPtr, + * u16 terminReason, ICMPReport *icmpMsg) + * + * TODO(phase2): push args right-to-left on the emulated stack + * (EM_A7), push a magic return address, execute68K, restore A7. + * Check whether Executor already has a generic pascal-call helper + * before hand-rolling one here (the menu/control defproc callers + * are the place to look). + */ +[[maybe_unused]] void call_asr(uint32_t cookie, MacTCPStream &s, + uint16_t event, uint16_t reason) +{ + if(!s.notifyProc) + return; + (void)cookie; + (void)event; + (void)reason; + warning_unimplemented("MacTCP ASR delivery (event %d)", event); +} + +/* ---- helpers ------------------------------------------------------- */ + +int set_nonblocking(int fd) +{ + int fl = fcntl(fd, F_GETFL, 0); + return fl < 0 ? fl : fcntl(fd, F_SETFL, fl | O_NONBLOCK); +} + +/* Wait for fd readiness with a MacTCP-style timeout (0 = default). */ +int poll_one(int fd, short events, uint8_t timeout_secs, + uint8_t default_secs) +{ + struct pollfd p = { fd, events, 0 }; + int secs = timeout_secs ? timeout_secs : default_secs; + return poll(&p, 1, secs * 1000); +} + +/* ---- csCode implementations (phase 1: synchronous) ---------------- */ + +OSErr do_getaddr(GetAddrParamBlock *pb) +{ + /* First non-loopback IPv4 interface. TODO: make configurable. */ + struct ifaddrs *ifa0; + OSErr err = ipBadCnfgErr; + + pb->ourAddress = 0; + pb->ourNetMask = 0; + if(getifaddrs(&ifa0) == 0) + { + for(struct ifaddrs *ifa = ifa0; ifa; ifa = ifa->ifa_next) + { + if(!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) + continue; + auto *sin = (struct sockaddr_in *)ifa->ifa_addr; + uint32_t a = ntohl(sin->sin_addr.s_addr); + if((a >> 24) == 127) + continue; + pb->ourAddress = a; + if(ifa->ifa_netmask) + pb->ourNetMask = ntohl( + ((struct sockaddr_in *)ifa->ifa_netmask) + ->sin_addr.s_addr); + err = noErr; + break; + } + freeifaddrs(ifa0); + } + return err; +} + +OSErr do_tcp_create(TCPiopb *pb) +{ + auto &create = pb->csParam.create; + + if(!create.rcvBuff || create.rcvBuffLen < 4096) + return invalidBufPtr; /* MacTCP demanded >= 4K rcv buffer */ + + uint32_t cookie = STREAM_COOKIE_BASE + next_stream_id++; + MacTCPStream &s = streams[cookie]; + s.rcvBuff = create.rcvBuff; + s.rcvBuffLen = create.rcvBuffLen; + s.notifyProc = guest_cast(create.notifyProc); + s.userDataPtr = create.userDataPtr; + + pb->tcpStream = guest_cast(cookie); + return noErr; +} + +OSErr do_tcp_active_open(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd >= 0) + return connectionExists; + + auto &open = pb->csParam.open; + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if(fd < 0) + return insufficientResources; + set_nonblocking(fd); + + if(open.localPort) + { + struct sockaddr_in la = {}; + la.sin_family = AF_INET; + la.sin_port = htons(open.localPort); + la.sin_addr.s_addr = htonl(open.localHost); + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + if(bind(fd, (struct sockaddr *)&la, sizeof(la)) < 0) + { + err = map_socket_errno(errno); + close(fd); + return err; + } + } + + struct sockaddr_in ra = {}; + ra.sin_family = AF_INET; + ra.sin_port = htons(open.remotePort); + ra.sin_addr.s_addr = htonl(open.remoteHost); + + if(connect(fd, (struct sockaddr *)&ra, sizeof(ra)) < 0 + && errno != EINPROGRESS) + { + err = map_socket_errno(errno); + close(fd); + return err; + } + + /* Phase 1: block right here until connected or ULP timeout. + * Phase 2: return inProgress, finish from the accRun pump. */ + int r = poll_one(fd, POLLOUT, open.ulpTimeoutValue, 60); + if(r <= 0) + { + close(fd); + return r == 0 ? openFailed : map_socket_errno(errno); + } + int soerr = 0; + socklen_t slen = sizeof(soerr); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &slen); + if(soerr) + { + close(fd); + return map_socket_errno(soerr); + } + + /* Report the resolved local endpoint back, as MacTCP did. */ + struct sockaddr_in la = {}; + socklen_t lalen = sizeof(la); + if(getsockname(fd, (struct sockaddr *)&la, &lalen) == 0) + { + open.localHost = ntohl(la.sin_addr.s_addr); + open.localPort = ntohs(la.sin_port); + } + + s->fd = fd; + s->state = StreamState::established; + return noErr; +} + +OSErr do_tcp_send(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd < 0 || s->state != StreamState::established) + return connectionDoesntExist; + + wdsEntry *w = guest_cast(pb->csParam.send.wdsPtr); + if(!w) + return invalidWDS; + + /* Gather the WDS (zero-length entry terminates the list). */ + struct iovec iov[16]; + int n = 0; + uint32_t total = 0; + for(; w[n].length && n < 16; ++n) + { + iov[n].iov_base = (void *)guest_cast(w[n].ptr); + iov[n].iov_len = w[n].length; + total += w[n].length; + } + if(w[n].length) + return invalidWDS; /* TODO: dynamically sized iovec */ + + /* Phase 1: loop until fully written (sockets are non-blocking). */ + uint32_t written = 0; + while(written < total) + { + ssize_t r = writev(s->fd, iov, n); + if(r < 0) + { + if(errno == EAGAIN || errno == EWOULDBLOCK) + { + if(poll_one(s->fd, POLLOUT, + pb->csParam.send.ulpTimeoutValue, 60) + <= 0) + return commandTimeout; + continue; + } + return map_socket_errno(errno); + } + written += r; + /* Advance iov past what was written. */ + while(r > 0 && n > 0) + { + if((size_t)r >= iov[0].iov_len) + { + r -= iov[0].iov_len; + std::memmove(&iov[0], &iov[1], + sizeof(iov[0]) * --n); + } + else + { + iov[0].iov_base = (char *)iov[0].iov_base + r; + iov[0].iov_len -= r; + r = 0; + } + } + } + if(pb->csParam.send.pushFlag) + { + int one = 1; + setsockopt(s->fd, IPPROTO_TCP, TCP_NODELAY, &one, + sizeof(one)); + } + return noErr; +} + +OSErr do_tcp_rcv(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd < 0) + return connectionDoesntExist; + + auto &rcv = pb->csParam.receive; + Ptr buf = rcv.rcvBuff; + uint16_t want = rcv.rcvBuffLen; + if(!buf || !want) + return invalidBufPtr; + + if(poll_one(s->fd, POLLIN, rcv.commandTimeoutValue, 60) <= 0) + return commandTimeout; + + ssize_t r = read(s->fd, (void *)buf, want); + if(r < 0) + return map_socket_errno(errno); + if(r == 0) + { + /* Peer FIN: report closing; further reads after drain + * report connectionTerminated per MacTCP semantics. */ + s->remoteClosed = true; + rcv.rcvBuffLen = 0; + return connectionClosing; + } + rcv.rcvBuffLen = (uint16_t)r; + rcv.urgentFlag = 0; + rcv.markFlag = 0; + return noErr; +} + +OSErr do_tcp_close(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd < 0) + return connectionDoesntExist; + shutdown(s->fd, SHUT_WR); /* half-close; stream stays readable */ + s->state = StreamState::closing; + return noErr; +} + +OSErr do_tcp_abort(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd >= 0) + { + struct linger lg = { 1, 0 }; /* RST on close */ + setsockopt(s->fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + close(s->fd); + s->fd = -1; + } + s->state = StreamState::terminated; + return noErr; +} + +OSErr do_tcp_status(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + + auto &st = pb->csParam.status; + std::memset((void *)&st, 0, sizeof(st)); + + switch(s->state) + { + case StreamState::established: + st.connectionState = TCPSEstablished; + break; + case StreamState::closing: + st.connectionState = s->remoteClosed ? TCPSClosing + : TCPSFinWait1; + break; + case StreamState::terminated: + case StreamState::created: + default: + st.connectionState = TCPSClosed; + break; + } + if(s->fd >= 0) + { + int avail = 0; + if(ioctl(s->fd, FIONREAD, &avail) == 0) + st.amtUnreadData = (uint16_t)std::min(avail, 0xffff); + + struct sockaddr_in a = {}; + socklen_t alen = sizeof(a); + if(getpeername(s->fd, (struct sockaddr *)&a, &alen) == 0) + { + st.remoteHost = ntohl(a.sin_addr.s_addr); + st.remotePort = ntohs(a.sin_port); + } + alen = sizeof(a); + if(getsockname(s->fd, (struct sockaddr *)&a, &alen) == 0) + { + st.localHost = ntohl(a.sin_addr.s_addr); + st.localPort = ntohs(a.sin_port); + } + } + return noErr; +} + +OSErr do_tcp_release(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd >= 0) + close(s->fd); + /* Hand the receive area back to the app, as MacTCP documented. */ + pb->csParam.create.rcvBuff = s->rcvBuff; + pb->csParam.create.rcvBuffLen = s->rcvBuffLen; + streams.erase(guest_cast(pb->tcpStream)); + return noErr; +} + +/* ---- accRun pump --------------------------------------------------- + * Called every SystemTask via NEEDTIMEBIT (see driver open). + * Phase 1: nothing to do — everything is synchronous. + * Phase 2: poll(2) all live fds; complete pending async ops; + * deliver TCPDataArrival / TCPClosing / TCPTerminate ASRs. + */ +void pump() +{ + /* TODO(phase2) */ +} + +} /* anonymous namespace */ + +/* ---- driver entry points ------------------------------------------ + * These live at file scope rather than in the anonymous namespace + * above because REGISTER_FUNCTION_PTR expands to an explicit template + * instantiation, which has to be at namespace scope. Same shape as + * serial.cpp: a C_-prefixed implementation plus a wrapper object of + * the register-convention calling convention the Device Manager + * dispatches through. + */ + +static OSErr C_ROMlib_ippopen(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippopen, D0(A0, A1)); +static OSErr C_ROMlib_ippprime(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippprime, D0(A0, A1)); +static OSErr C_ROMlib_ippctl(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippctl, D0(A0, A1)); +static OSErr C_ROMlib_ippstatus(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippstatus, D0(A0, A1)); +static OSErr C_ROMlib_ippclose(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippclose, D0(A0, A1)); + +static OSErr C_ROMlib_ippopen(ParmBlkPtr pbp, DCtlPtr dce) +{ + (void)pbp; + /* Ask C_SystemTask to deliver accRun to us every pass. */ + dce->dCtlFlags |= NEEDTIMEBIT; + dce->dCtlDelay = 0; + return noErr; +} + +static OSErr C_ROMlib_ippprime(ParmBlkPtr pbp, DCtlPtr dce) +{ + /* MacTCP has no Read/Write interface; everything is Control. */ + (void)pbp; + (void)dce; + return controlErr; +} + +static OSErr C_ROMlib_ippctl(ParmBlkPtr pbp, DCtlPtr dce) +{ + (void)dce; + TCPiopb *pb = (TCPiopb *)pbp; + OSErr err; + + switch(pb->csCode) + { + case accRun: + pump(); + return noErr; /* housekeeping: no completion semantics */ + + case killCode: + /* Phase 1 has no queued ops to kill. */ + return complete(pb, noErr); + + case ipctlGetAddr: + err = do_getaddr((GetAddrParamBlock *)pbp); + break; + + case TCPCreate: + err = do_tcp_create(pb); + break; + case TCPActiveOpen: + err = do_tcp_active_open(pb); + break; + case TCPSend: + err = do_tcp_send(pb); + break; + case TCPRcv: + err = do_tcp_rcv(pb); + break; + case TCPClose: + err = do_tcp_close(pb); + break; + case TCPAbort: + err = do_tcp_abort(pb); + break; + case TCPStatus: + err = do_tcp_status(pb); + break; + case TCPRelease: + err = do_tcp_release(pb); + break; + + case TCPPassiveOpen: /* phase 3 */ + case TCPNoCopyRcv: /* phase 2 */ + case TCPRcvBfrReturn: /* phase 2 */ + case udpCreate: /* phase 3 ... */ + case udpRead: + case udpBfrReturn: + case udpWrite: + case udpRelease: + case udpMaxMTUSize: + case udpStatus: + warning_unimplemented("MacTCP csCode %d", (int)pb->csCode); + err = invalidLength; /* TODO: most fitting stub error? */ + break; + + default: + warning_unexpected("MacTCP unknown csCode %d", + (int)pb->csCode); + err = controlErr; + break; + } + return complete(pb, err); +} + +static OSErr C_ROMlib_ippstatus(ParmBlkPtr pbp, DCtlPtr dce) +{ + /* MacTCP routes its status-ish calls (TCPStatus etc.) through + * Control; a bare PBStatus on .IPP has nothing to report. */ + (void)dce; + return complete((TCPiopb *)pbp, controlErr); +} + +static OSErr C_ROMlib_ippclose(ParmBlkPtr pbp, DCtlPtr dce) +{ + (void)pbp; + (void)dce; + for(auto &kv : streams) + if(kv.second.fd >= 0) + close(kv.second.fd); + streams.clear(); + return noErr; +} + +/* ---- registration -------------------------------------------------- + * WIRE-UP: add `InitMacTCPDriver();` to InitBuiltinDrivers() in + * device.cpp. + * + * Unit slot: 47 (refnum -48), the top of the 48-entry unit table — + * far from .Sony (-5) and the serial units (-6..-9). Real MacTCP + * grabbed whatever free unit the Device Manager gave it, and no + * application may depend on the number: they must PBOpen by name. + */ +void Executor::InitMacTCPDriver() +{ + RegisterDriver({ + &ROMlib_ippopen, &ROMlib_ippprime, &ROMlib_ippctl, + &ROMlib_ippstatus, &ROMlib_ippclose, + (StringPtr) "\04.IPP", -48, + }); +} From c28b9c851c27bade7efdb575306c56f48515d13b Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 13:18:49 -0700 Subject: [PATCH 4/8] Add runtime tests for the MacTCP driver Until now the driver had only been verified statically -- it compiled, linked and registered, but no part of it had ever executed. main_executor.cpp already stands up memory, the 68k emulator, traps and low memory in a native gtest binary, which is enough to drive the driver through the real OpenDriver/PBControl path with no 68k application involved. These tests use that: name lookup and the unit slot, ipctlGetAddr, rejection of undersized receive buffers, unknown csCodes and unknown streams, and a full TCPCreate / TCPActiveOpen / TCPSend / TCPRcv / TCPStatus / TCPClose / TCPRelease round trip. The round trip is hermetic: the test process itself listens on a loopback socket and the driver connects back to it, so nothing depends on the host having a network, a route or a resolver. The send goes through a multi-entry WDS so the gather path is covered rather than just the single-buffer case. ipctlGetAddr skips rather than fails on a host with no non-loopback IPv4 interface, which is the normal situation in a container. Native-only for now. The dual-mode offsetof/sizeof comparison against Apple's Universal Interfaces belongs in TEST_SOURCES once the Retro68 side can generate MacTCP.h, and is what will settle the VERIFY markers still in MacTCP.yaml. --- tests/CMakeLists.txt | 4 + tests/mactcp.cpp | 365 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+) create mode 100644 tests/mactcp.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2d52cc19..c5e8ea2c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,10 @@ endfunction() set(NATIVE_TEST_SOURCES files_internal.cpp guestvalues.cpp cpu.cpp sane.cpp + # Native-only: drives the .IPP driver through the Device Manager. + # The dual-mode ABI comparison against Apple's headers belongs in + # TEST_SOURCES later, once the Retro68 side has MacTCP.h. + mactcp.cpp ) set(TEST_SOURCES diff --git a/tests/mactcp.cpp b/tests/mactcp.cpp new file mode 100644 index 00000000..97de75d8 --- /dev/null +++ b/tests/mactcp.cpp @@ -0,0 +1,365 @@ +/* Runtime tests for the native MacTCP (.IPP) driver. + * + * Native-only: these drive the driver through the real Device Manager + * entry points (OpenDriver / PBControl) inside the test harness set up + * by main_executor.cpp, so no 68k application and no emulator window + * are involved. + * + * The TCP tests are hermetic -- the test process itself listens on a + * loopback socket and the driver connects back to it, so nothing here + * depends on the machine having a network, a route, or a name server. + */ + +#include "gtest/gtest.h" + +#include "compat.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +using namespace Executor; + +namespace +{ + +/* Buffers reachable by the driver have to live in guest memory: the + * driver turns the GUEST fields back into host pointers with + * guest_cast, which is only meaningful for addresses inside the guest + * address space. NewPtr gives us that; the host stack would not. */ +Ptr guestBuffer(uint32_t size) +{ + Ptr p = NewPtrClear(size); + EXPECT_NE(nullptr, p); + return p; +} + +INTEGER openIPP() +{ + GUEST refnum = 0; + OSErr err = OpenDriver(PSTR(".IPP"), &refnum); + EXPECT_EQ(noErr, err) << "OpenDriver(\".IPP\") failed"; + return refnum; +} + +/* Issue one PBControl against .IPP. The parameter block may live on + * the host stack: it is never handed to emulated code, because these + * are synchronous calls with no completion routine. */ +OSErr control(INTEGER refnum, TCPiopb& pb, INTEGER csCode) +{ + pb.ioCRefNum = refnum; + pb.csCode = csCode; + return PBControl((ParmBlkPtr)&pb, false); +} + +/* A host-side listening socket for the driver to connect to. */ +class LoopbackListener +{ +public: + LoopbackListener() + { + fd_ = socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(fd_, 0); + + struct sockaddr_in a = {}; + a.sin_family = AF_INET; + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + a.sin_port = 0; /* let the kernel pick */ + EXPECT_EQ(0, bind(fd_, (struct sockaddr *)&a, sizeof(a))); + EXPECT_EQ(0, listen(fd_, 1)); + + socklen_t len = sizeof(a); + EXPECT_EQ(0, getsockname(fd_, (struct sockaddr *)&a, &len)); + port_ = ntohs(a.sin_port); + } + + ~LoopbackListener() + { + if(accepted_ >= 0) + close(accepted_); + if(fd_ >= 0) + close(fd_); + } + + uint16_t port() const { return port_; } + + int accepted() + { + if(accepted_ < 0) + accepted_ = accept(fd_, nullptr, nullptr); + return accepted_; + } + +private: + int fd_ = -1; + int accepted_ = -1; + uint16_t port_ = 0; +}; + +/* Open a stream and connect it to the listener. Returns the refnum; + * the stream cookie is left in pb.tcpStream. */ +void createAndConnect(INTEGER refnum, TCPiopb& pb, Ptr rcvBuff, + uint32_t rcvBuffLen, uint16_t port) +{ + std::memset(&pb, 0, sizeof(pb)); + pb.csParam.create.rcvBuff = rcvBuff; + pb.csParam.create.rcvBuffLen = rcvBuffLen; + ASSERT_EQ(noErr, control(refnum, pb, TCPCreate)); + ASSERT_NE(nullptr, pb.tcpStream) << "TCPCreate returned a null stream"; + + auto stream = pb.tcpStream; + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.open.remoteHost = 0x7F000001; /* 127.0.0.1, numeric */ + pb.csParam.open.remotePort = port; + pb.csParam.open.ulpTimeoutValue = 5; + ASSERT_EQ(noErr, control(refnum, pb, TCPActiveOpen)); +} + +} /* namespace */ + +/* ---- step 2: the driver is reachable at all ----------------------- */ + +TEST(MacTCP, OpenDriverByName) +{ + GUEST refnum = 0; + + ASSERT_EQ(noErr, OpenDriver(PSTR(".IPP"), &refnum)); + /* Registered at refnum -48 == unit 47, the top of the unit table. */ + EXPECT_EQ(-48, (INTEGER)refnum); + EXPECT_NE(nullptr, GetDCtlEntry(refnum)); +} + +TEST(MacTCP, OpenDriverIsCaseInsensitive) +{ + GUEST refnum = 0; + + /* ROMlib_driveropen matches driver names case-insensitively. */ + ASSERT_EQ(noErr, OpenDriver(PSTR(".ipp"), &refnum)); + EXPECT_EQ(-48, (INTEGER)refnum); +} + +TEST(MacTCP, GetAddrReportsAnInterface) +{ + INTEGER refnum = openIPP(); + + GetAddrParamBlock pb; + std::memset(&pb, 0, sizeof(pb)); + pb.ioCRefNum = refnum; + pb.csCode = ipctlGetAddr; + + OSErr err = PBControl((ParmBlkPtr)&pb, false); + + /* A machine with no non-loopback IPv4 interface is a legitimate + * configuration (CI containers, in particular), and the driver + * reports that rather than inventing an address. */ + if(err == ipBadCnfgErr) + { + EXPECT_EQ(0u, (uint32_t)pb.ourAddress); + GTEST_SKIP() << "no non-loopback IPv4 interface on this host"; + } + + ASSERT_EQ(noErr, err); + uint32_t addr = pb.ourAddress; + EXPECT_NE(0u, addr) << "noErr but no address filled in"; + EXPECT_NE(127u, addr >> 24) << "loopback should have been skipped"; +} + +TEST(MacTCP, UnknownCsCodeIsRejected) +{ + INTEGER refnum = openIPP(); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + EXPECT_EQ(controlErr, control(refnum, pb, 999)); +} + +/* ---- step 3: a hermetic loopback round trip ----------------------- */ + +TEST(MacTCP, CreateRejectsUndersizedBuffer) +{ + INTEGER refnum = openIPP(); + Ptr small = guestBuffer(1024); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + pb.csParam.create.rcvBuff = small; + pb.csParam.create.rcvBuffLen = 1024; + + /* MacTCP required a receive buffer of at least 4K. */ + EXPECT_EQ(invalidBufPtr, control(refnum, pb, TCPCreate)); + + DisposePtr(small); +} + +TEST(MacTCP, OperationsOnAnUnknownStreamFail) +{ + INTEGER refnum = openIPP(); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + + /* A StreamPtr is an opaque cookie, not a real address, so it has to + * be built with guest_cast the same way the driver builds it. + * Assigning a fabricated host pointer instead would send + * US_TO_SYN68K an address outside the guest space and abort. */ + pb.tcpStream = guest_cast(0xDEADBEEFu); + + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPStatus)); + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPClose)); + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPRelease)); + + /* Zero is what a parameter block that was never filled in holds. */ + std::memset(&pb, 0, sizeof(pb)); + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPStatus)); +} + +TEST(MacTCP, LoopbackRoundTrip) +{ + INTEGER refnum = openIPP(); + LoopbackListener listener; + + const uint32_t kRcvBuffLen = 8192; + Ptr rcvBuff = guestBuffer(kRcvBuffLen); + + TCPiopb pb; + ASSERT_NO_FATAL_FAILURE( + createAndConnect(refnum, pb, rcvBuff, kRcvBuffLen, listener.port())); + auto stream = pb.tcpStream; + + int peer = listener.accepted(); + ASSERT_GE(peer, 0) << "driver never connected"; + + /* TCPActiveOpen should have reported the local endpoint back. */ + EXPECT_NE(0u, (uint32_t)pb.csParam.open.localPort); + + /* --- send, exercising the WDS gather list ---------------------- */ + const char part1[] = "GET / "; + const char part2[] = "HTTP/1.0\r\n\r\n"; + const char whole[] = "GET / HTTP/1.0\r\n\r\n"; + + Ptr b1 = guestBuffer(sizeof(part1)); + Ptr b2 = guestBuffer(sizeof(part2)); + std::memcpy(b1, part1, sizeof(part1) - 1); + std::memcpy(b2, part2, sizeof(part2) - 1); + + /* Three entries: two of data, then the zero-length terminator. */ + Ptr wdsMem = guestBuffer(sizeof(wdsEntry) * 3); + auto *wds = (wdsEntry *)wdsMem; + wds[0].length = sizeof(part1) - 1; + wds[0].ptr = b1; + wds[1].length = sizeof(part2) - 1; + wds[1].ptr = b2; + wds[2].length = 0; + wds[2].ptr = nullptr; + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.send.wdsPtr = wdsMem; + pb.csParam.send.pushFlag = 1; + pb.csParam.send.ulpTimeoutValue = 5; + ASSERT_EQ(noErr, control(refnum, pb, TCPSend)); + + char got[64] = {}; + ssize_t n = recv(peer, got, sizeof(got), 0); + ASSERT_EQ((ssize_t)(sizeof(whole) - 1), n) + << "gathered send did not arrive whole"; + EXPECT_EQ(0, std::memcmp(got, whole, sizeof(whole) - 1)); + + /* --- receive --------------------------------------------------- */ + const char reply[] = "HTTP/1.0 200 OK\r\n"; + ASSERT_EQ((ssize_t)(sizeof(reply) - 1), + send(peer, reply, sizeof(reply) - 1, 0)); + + Ptr appBuff = guestBuffer(256); + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.receive.rcvBuff = appBuff; + pb.csParam.receive.rcvBuffLen = 256; + pb.csParam.receive.commandTimeoutValue = 5; + ASSERT_EQ(noErr, control(refnum, pb, TCPRcv)); + + EXPECT_EQ(sizeof(reply) - 1, (size_t)(uint16_t)pb.csParam.receive.rcvBuffLen); + EXPECT_EQ(0, std::memcmp(appBuff, reply, sizeof(reply) - 1)); + + /* --- status ---------------------------------------------------- */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPStatus)); + EXPECT_EQ(TCPSEstablished, (int)pb.csParam.status.connectionState); + EXPECT_EQ(0x7F000001u, (uint32_t)pb.csParam.status.remoteHost); + EXPECT_EQ(listener.port(), (uint16_t)pb.csParam.status.remotePort); + + /* --- close ----------------------------------------------------- */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPClose)); + + /* Our FIN should reach the peer as end-of-stream. */ + char drain[16]; + EXPECT_EQ(0, recv(peer, drain, sizeof(drain), 0)); + + /* --- release --------------------------------------------------- */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPRelease)); + /* Release hands the receive area back to the application. */ + EXPECT_EQ(rcvBuff, (Ptr)pb.csParam.create.rcvBuff); + EXPECT_EQ(kRcvBuffLen, (uint32_t)pb.csParam.create.rcvBuffLen); + + /* The stream is gone afterwards. */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPStatus)); + + DisposePtr(appBuff); + DisposePtr(wdsMem); + DisposePtr(b2); + DisposePtr(b1); + DisposePtr(rcvBuff); +} + +TEST(MacTCP, PeerCloseIsReportedAsConnectionClosing) +{ + INTEGER refnum = openIPP(); + LoopbackListener listener; + + const uint32_t kRcvBuffLen = 8192; + Ptr rcvBuff = guestBuffer(kRcvBuffLen); + + TCPiopb pb; + ASSERT_NO_FATAL_FAILURE( + createAndConnect(refnum, pb, rcvBuff, kRcvBuffLen, listener.port())); + auto stream = pb.tcpStream; + + int peer = listener.accepted(); + ASSERT_GE(peer, 0); + + /* Peer hangs up without sending anything. */ + shutdown(peer, SHUT_WR); + + Ptr appBuff = guestBuffer(256); + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.receive.rcvBuff = appBuff; + pb.csParam.receive.rcvBuffLen = 256; + pb.csParam.receive.commandTimeoutValue = 5; + + EXPECT_EQ(connectionClosing, control(refnum, pb, TCPRcv)); + EXPECT_EQ(0, (uint16_t)pb.csParam.receive.rcvBuffLen); + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + EXPECT_EQ(noErr, control(refnum, pb, TCPAbort)); + + DisposePtr(appBuff); + DisposePtr(rcvBuff); +} From 63b179179c9c81db84a4936e86337ceb6d69a630 Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 13:55:26 -0700 Subject: [PATCH 5/8] Add a dual-mode MacTCP ABI conformance test Checks the size and every byte offset of the parameter blocks an application shares with the .IPP driver, both at compile time and at runtime, plus the result codes, csCodes and connection states. The two build modes answer two different questions. Natively it checks that multiversal's generated header really lays out the way defs/MacTCP.yaml says: the YAML's size: assertions only pin totals, so a mac68k alignment mistake in the middle of a struct that preserves the total would otherwise go unnoticed. Under Retro68, compiled against Apple's Universal Interfaces, it checks the numbers taken from the MacTCP Programmer's Guide against Apple's own -- which is the ground truth that settles the VERIFY markers still in the YAML. The expected values are written out here rather than derived from the YAML, so a transcription error in the YAML surfaces as a failure instead of being mirrored. DumpLayout prints the whole layout in a diffable form for comparing the two sides directly, since an assertion tells you something is wrong but not what the other side thinks it should be. The file compiles to nothing where there is no MacTCP.h, so it does not break Retro68 builds without Apple's interfaces installed. --- tests/CMakeLists.txt | 6 + tests/mactcp_abi.cpp | 326 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 tests/mactcp_abi.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c5e8ea2c..bf1456f5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -47,6 +47,12 @@ set(NATIVE_TEST_SOURCES set(TEST_SOURCES files.cpp quickdraw.cpp listmgr.cpp resources.cpp events.cpp + # Dual-mode on purpose: natively this checks the generated + # headers against defs/MacTCP.yaml, and under Retro68 -- built + # against Apple's Universal Interfaces rather than generated + # CIncludes -- it checks the documented ABI against Apple's. + # Compiles to nothing where there is no MacTCP.h. + mactcp_abi.cpp ) if(CMAKE_SYSTEM_NAME STREQUAL Retro68) list(APPEND TEST_SOURCES diff --git a/tests/mactcp_abi.cpp b/tests/mactcp_abi.cpp new file mode 100644 index 00000000..c53c3a2a --- /dev/null +++ b/tests/mactcp_abi.cpp @@ -0,0 +1,326 @@ +/* MacTCP ABI conformance: sizes and byte offsets of the parameter + * blocks an application shares with the .IPP driver. + * + * This file is deliberately dual-mode, and the two modes answer two + * *different* questions: + * + * Native (-DEXECUTOR), against multiversal's generated MacTCP.h: + * does the header the generator produced actually lay out the + * way defs/MacTCP.yaml says it does? The `size:` assertions in + * the YAML only pin total sizes, so a mac68k alignment mistake + * in the middle of a struct that happens to preserve the total + * can slip through. These per-field offsets catch that. + * + * Retro68, against Apple's Universal Interfaces MacTCP.h: + * are the numbers we took from the MacTCP Programmer's Guide + * actually correct? This is the ground truth, and it is the + * half that settles the VERIFY markers in the YAML. + * + * IMPORTANT: the Retro68 half only means something when it compiles + * against *Apple's* headers. Retro68 generates its CIncludes from the + * same multiversal YAML that Executor uses, so if MacTCP.yaml is ever + * added to Retro68's multiversal too, building this against those + * generated CIncludes would compare our definitions to themselves and + * prove nothing. Genuine Universal Interfaces must come first on the + * include path. + * + * Today that happens by default: Retro68's multiversal has no MacTCP + * definitions at all, so there can only be Apple's, which + * arrives via Retro68's interfaces-and-libraries.sh from a copy of + * Apple's Universal Interfaces you supply yourself. Retro68's own + * LaunchAPPL/Server/MacTCPStream.cc is a working MacTCP client built + * that way, and every parameter block field it touches agrees with the + * names used here. + * + * To settle the VERIFY markers: + * + * ./tests --gtest_filter=MacTCPABI.DumpLayout # native, ours + * ... same test built as the Retro68 application, run under + * Basilisk II or on real hardware via LaunchAPPL ... + * diff ours.txt apples.txt + * + * A compile failure on the Retro68 side is itself a result: it means a + * field name in defs/MacTCP.yaml does not match Apple's. + * + * The expected values below come from the MacTCP Programmer's Guide, + * not from the YAML, so that a transcription error in the YAML shows + * up as a failure here rather than being quietly mirrored. + */ + +#include "gtest/gtest.h" + +#include "compat.h" + +/* Retro68 builds that have no MacTCP.h at all shouldn't fail to build; + * they just can't answer the question. */ +#if !defined(__has_include) +#define MACTCP_ABI_HAVE_HEADER 1 +#elif __has_include() +#define MACTCP_ABI_HAVE_HEADER 1 +#else +#define MACTCP_ABI_HAVE_HEADER 0 +#endif + +#if MACTCP_ABI_HAVE_HEADER + +#include + +#include +#include + +#ifdef EXECUTOR +using namespace Executor; +#endif + +/* Absolute offsets inside TCPiopb. Reaching the csParam members + * through the enclosing block rather than through each sub-struct + * keeps this independent of what the sub-structs are named, and + * absolute offsets are what the ABI actually constrains. + * + * X(field-expression, expected-offset) + */ +#define MACTCP_IOPB_FIELDS(X) \ + X(qLink, 0) \ + X(qType, 4) \ + X(ioTrap, 6) \ + X(ioCmdAddr, 8) \ + X(ioCompletion, 12) \ + X(ioResult, 16) \ + X(ioNamePtr, 18) \ + X(ioVRefNum, 22) \ + X(ioCRefNum, 24) \ + X(csCode, 26) \ + X(tcpStream, 28) \ + /* TCPCreate */ \ + X(csParam.create.rcvBuff, 32) \ + X(csParam.create.rcvBuffLen, 36) \ + X(csParam.create.notifyProc, 40) \ + X(csParam.create.userDataPtr, 44) \ + /* TCPActiveOpen / TCPPassiveOpen */ \ + X(csParam.open.ulpTimeoutValue, 32) \ + X(csParam.open.ulpTimeoutAction, 33) \ + X(csParam.open.validityFlags, 34) \ + X(csParam.open.commandTimeoutValue, 35)\ + X(csParam.open.remoteHost, 36) \ + X(csParam.open.remotePort, 40) \ + X(csParam.open.localHost, 42) \ + X(csParam.open.localPort, 46) \ + X(csParam.open.tosFlags, 48) \ + X(csParam.open.precedence, 49) \ + X(csParam.open.dontFrag, 50) \ + X(csParam.open.timeToLive, 51) \ + X(csParam.open.security, 52) \ + X(csParam.open.optionCnt, 53) \ + X(csParam.open.options, 54) \ + X(csParam.open.userDataPtr, 94) \ + /* TCPSend -- VERIFY: position of the filler at 37 */ \ + X(csParam.send.ulpTimeoutValue, 32) \ + X(csParam.send.ulpTimeoutAction, 33) \ + X(csParam.send.validityFlags, 34) \ + X(csParam.send.pushFlag, 35) \ + X(csParam.send.urgentFlag, 36) \ + X(csParam.send.wdsPtr, 38) \ + X(csParam.send.sendFree, 42) \ + X(csParam.send.sendLength, 46) \ + X(csParam.send.userDataPtr, 48) \ + /* TCPRcv / TCPNoCopyRcv / TCPRcvBfrReturn \ + * VERIFY: relative order of urgentFlag and markFlag at 48/49 */ \ + X(csParam.receive.commandTimeoutValue, 32) \ + X(csParam.receive.rcvBuff, 34) \ + X(csParam.receive.rcvBuffLen, 38) \ + X(csParam.receive.rdsPtr, 40) \ + X(csParam.receive.rdsLength, 44) \ + X(csParam.receive.secondTimeStamp, 46) \ + X(csParam.receive.urgentFlag, 48) \ + X(csParam.receive.markFlag, 49) \ + X(csParam.receive.userDataPtr, 50) \ + /* TCPClose */ \ + X(csParam.close.ulpTimeoutValue, 32) \ + X(csParam.close.ulpTimeoutAction, 33) \ + X(csParam.close.validityFlags, 34) \ + X(csParam.close.userDataPtr, 36) \ + /* TCPAbort */ \ + X(csParam.abort.userDataPtr, 32) \ + /* TCPStatus -- VERIFY: the tail from srtt at 82 onwards */ \ + X(csParam.status.ulpTimeoutValue, 32) \ + X(csParam.status.ulpTimeoutAction, 33) \ + X(csParam.status.remoteHost, 38) \ + X(csParam.status.remotePort, 42) \ + X(csParam.status.localHost, 44) \ + X(csParam.status.localPort, 48) \ + X(csParam.status.tosFlags, 50) \ + X(csParam.status.precedence, 51) \ + X(csParam.status.connectionState, 52) \ + X(csParam.status.sendWindow, 54) \ + X(csParam.status.rcvWindow, 56) \ + X(csParam.status.amtUnackedData, 58) \ + X(csParam.status.amtUnreadData, 60) \ + X(csParam.status.securityLevelPtr, 62) \ + X(csParam.status.sendUnacked, 66) \ + X(csParam.status.sendNext, 70) \ + X(csParam.status.congestionWindow, 74) \ + X(csParam.status.rcvNext, 78) \ + X(csParam.status.srtt, 82) \ + X(csParam.status.lastRtt, 86) \ + X(csParam.status.sendMaxSegSize, 90) \ + X(csParam.status.userDataPtr, 94) + +#define MACTCP_GETADDR_FIELDS(X) \ + X(ourAddress, 28) \ + X(ourNetMask, 32) + +/* Compile-time is the strongest form: a mismatch stops the build + * rather than waiting for someone to run the suite. */ +#define ASSERT_IOPB_OFFSET(field, expected) \ + static_assert(offsetof(TCPiopb, field) == (expected), "TCPiopb::" #field); +MACTCP_IOPB_FIELDS(ASSERT_IOPB_OFFSET) +#undef ASSERT_IOPB_OFFSET + +#define ASSERT_GETADDR_OFFSET(field, expected) \ + static_assert(offsetof(GetAddrParamBlock, field) == (expected), \ + "GetAddrParamBlock::" #field); +MACTCP_GETADDR_FIELDS(ASSERT_GETADDR_OFFSET) +#undef ASSERT_GETADDR_OFFSET + +static_assert(sizeof(TCPiopb) == 98, "TCPiopb size"); +static_assert(sizeof(GetAddrParamBlock) == 36, "GetAddrParamBlock size"); +static_assert(sizeof(wdsEntry) == 6, "wdsEntry size"); +static_assert(sizeof(rdsEntry) == 6, "rdsEntry size"); +static_assert(sizeof(ICMPReport) == 24, "ICMPReport size"); + +/* The same checks again at runtime. Compile-time assertions stop at + * the first failure and print no numbers; these report every + * discrepancy with both values, which is what you want when diffing + * our headers against Apple's. */ +TEST(MacTCPABI, TCPiopbFieldOffsets) +{ +#define CHECK_IOPB_OFFSET(field, expected) \ + EXPECT_EQ((size_t)(expected), offsetof(TCPiopb, field)) << "TCPiopb::" #field; + MACTCP_IOPB_FIELDS(CHECK_IOPB_OFFSET) +#undef CHECK_IOPB_OFFSET +} + +TEST(MacTCPABI, GetAddrParamBlockFieldOffsets) +{ +#define CHECK_GETADDR_OFFSET(field, expected) \ + EXPECT_EQ((size_t)(expected), offsetof(GetAddrParamBlock, field)) \ + << "GetAddrParamBlock::" #field; + MACTCP_GETADDR_FIELDS(CHECK_GETADDR_OFFSET) +#undef CHECK_GETADDR_OFFSET +} + +TEST(MacTCPABI, StructSizes) +{ + EXPECT_EQ(98u, (unsigned)sizeof(TCPiopb)); + EXPECT_EQ(36u, (unsigned)sizeof(GetAddrParamBlock)); + EXPECT_EQ(6u, (unsigned)sizeof(wdsEntry)); + EXPECT_EQ(6u, (unsigned)sizeof(rdsEntry)); + EXPECT_EQ(24u, (unsigned)sizeof(ICMPReport)); + + /* csParam has to cover the largest member; open and status are + * tied at 66 bytes, which is what makes TCPiopb 98. */ + EXPECT_EQ(66u, (unsigned)(sizeof(TCPiopb) - 32)); +} + +/* Result codes are as much a part of the ABI as the layouts: an + * application switching on them cares about the exact values. */ +TEST(MacTCPABI, ResultCodes) +{ + EXPECT_EQ(1, (int)inProgress); + EXPECT_EQ(-23000, (int)ipBadLapErr); + EXPECT_EQ(-23001, (int)ipBadCnfgErr); + EXPECT_EQ(-23002, (int)ipNoCnfgErr); + EXPECT_EQ(-23003, (int)ipLoadErr); + EXPECT_EQ(-23004, (int)ipBadAddr); + EXPECT_EQ(-23005, (int)connectionClosing); + EXPECT_EQ(-23006, (int)invalidLength); + EXPECT_EQ(-23007, (int)connectionExists); + EXPECT_EQ(-23008, (int)connectionDoesntExist); + EXPECT_EQ(-23009, (int)insufficientResources); + EXPECT_EQ(-23010, (int)invalidStreamPtr); + EXPECT_EQ(-23011, (int)streamAlreadyOpen); + EXPECT_EQ(-23012, (int)connectionTerminated); + EXPECT_EQ(-23013, (int)invalidBufPtr); + EXPECT_EQ(-23014, (int)invalidRDS); + EXPECT_EQ(-23014, (int)invalidWDS); + EXPECT_EQ(-23015, (int)openFailed); + EXPECT_EQ(-23016, (int)commandTimeout); + EXPECT_EQ(-23017, (int)duplicateSocket); + EXPECT_EQ(-23041, (int)nameSyntaxErr); + EXPECT_EQ(-23042, (int)cacheFault); + EXPECT_EQ(-23043, (int)noResultProc); + EXPECT_EQ(-23044, (int)noNameServer); + EXPECT_EQ(-23045, (int)authNameErr); + EXPECT_EQ(-23046, (int)noAnsErr); + EXPECT_EQ(-23047, (int)dnrErr); +} + +TEST(MacTCPABI, CsCodes) +{ + EXPECT_EQ(15, (int)ipctlGetAddr); + EXPECT_EQ(30, (int)TCPCreate); + EXPECT_EQ(31, (int)TCPPassiveOpen); + EXPECT_EQ(32, (int)TCPActiveOpen); + EXPECT_EQ(34, (int)TCPSend); + EXPECT_EQ(35, (int)TCPNoCopyRcv); + EXPECT_EQ(36, (int)TCPRcvBfrReturn); + EXPECT_EQ(37, (int)TCPRcv); + EXPECT_EQ(38, (int)TCPClose); + EXPECT_EQ(39, (int)TCPAbort); + EXPECT_EQ(40, (int)TCPStatus); + EXPECT_EQ(41, (int)TCPExtendedStat); + EXPECT_EQ(42, (int)TCPRelease); + EXPECT_EQ(43, (int)TCPGlobalInfo); +} + +TEST(MacTCPABI, ConnectionStates) +{ + /* All even; the odd values were never used. */ + EXPECT_EQ(0, (int)TCPSClosed); + EXPECT_EQ(2, (int)TCPSListen); + EXPECT_EQ(4, (int)TCPSSynReceived); + EXPECT_EQ(6, (int)TCPSSynSent); + EXPECT_EQ(8, (int)TCPSEstablished); + EXPECT_EQ(10, (int)TCPSFinWait1); + EXPECT_EQ(12, (int)TCPSFinWait2); + EXPECT_EQ(14, (int)TCPSCloseWait); + EXPECT_EQ(16, (int)TCPSClosing); + EXPECT_EQ(18, (int)TCPSLastAck); + EXPECT_EQ(20, (int)TCPSTimeWait); +} + +/* A diffable dump. Run this on both sides -- natively, and as the + * Retro68 application against Apple's headers -- and diff the two + * transcripts. Where the assertions above only say "wrong", this says + * what the other side actually thinks the layout is. + * + * ./tests --gtest_filter=MacTCPABI.DumpLayout > ours.txt + */ +TEST(MacTCPABI, DumpLayout) +{ + printf("sizeof(TCPiopb) = %u\n", (unsigned)sizeof(TCPiopb)); + printf("sizeof(GetAddrParamBlock) = %u\n", + (unsigned)sizeof(GetAddrParamBlock)); + printf("sizeof(wdsEntry) = %u\n", (unsigned)sizeof(wdsEntry)); + printf("sizeof(rdsEntry) = %u\n", (unsigned)sizeof(rdsEntry)); + printf("sizeof(ICMPReport) = %u\n", (unsigned)sizeof(ICMPReport)); + +#define DUMP_IOPB_OFFSET(field, expected) \ + printf("TCPiopb.%-40s = %u\n", #field, (unsigned)offsetof(TCPiopb, field)); + MACTCP_IOPB_FIELDS(DUMP_IOPB_OFFSET) +#undef DUMP_IOPB_OFFSET + +#define DUMP_GETADDR_OFFSET(field, expected) \ + printf("GetAddrParamBlock.%-31s = %u\n", #field, \ + (unsigned)offsetof(GetAddrParamBlock, field)); + MACTCP_GETADDR_FIELDS(DUMP_GETADDR_OFFSET) +#undef DUMP_GETADDR_OFFSET +} + +#else /* !MACTCP_ABI_HAVE_HEADER */ + +TEST(MacTCPABI, DISABLED_NoMacTCPHeader) +{ +} + +#endif From 13229f8367d151cc5565d717974a6ed3306d1f8d Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 14:09:05 -0700 Subject: [PATCH 6/8] Pin the MacTCP ABI to Apple's actual layouts Picks up the multiversal correction to TCPReceivePB and TCPStatusPB, and tightens the assertions on both sides to match. The driver's guard rails now cover the fields that were wrong rather than only the ones that happened to be right: TCPReceivePB's rcvBuff, markFlag and urgentFlag, plus the sizes of TCPStatusPB and TCPiopb, which changed from 66 and 98 to 70 and 102. Worth noting for anyone reading the earlier test results: the loopback round trip passed both before and after this fix. It writes the parameter block through the same header the driver reads it through, so a wrong offset is invisible to it -- both sides are wrong in the same place. Only a comparison against an independent definition of the ABI could catch this, which is what the dual-mode test is for. --- multiversal | 2 +- src/mactcp/mactcp.cpp | 6 ++++- tests/mactcp_abi.cpp | 61 +++++++++++++++++++++++++------------------ 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/multiversal b/multiversal index 53efee90..6832f1b2 160000 --- a/multiversal +++ b/multiversal @@ -1 +1 @@ -Subproject commit 53efee90c122ae8e8cd9158a89925141a25c724c +Subproject commit 6832f1b2aee838777d58951d05340a532e3be683 diff --git a/src/mactcp/mactcp.cpp b/src/mactcp/mactcp.cpp index 6be49a2e..b2171dfc 100644 --- a/src/mactcp/mactcp.cpp +++ b/src/mactcp/mactcp.cpp @@ -80,7 +80,11 @@ static_assert(offsetof(TCPiopb, csParam) == 32, "TCPiopb ABI"); static_assert(offsetof(TCPOpenPB, remoteHost) == 4, "TCPOpenPB ABI"); static_assert(offsetof(TCPOpenPB, localHost) == 10, "TCPOpenPB ABI"); static_assert(offsetof(TCPSendPB, wdsPtr) == 6, "TCPSendPB ABI"); -static_assert(offsetof(TCPReceivePB, rcvBuff) == 2, "TCPReceivePB ABI"); +static_assert(offsetof(TCPReceivePB, rcvBuff) == 4, "TCPReceivePB ABI"); +static_assert(offsetof(TCPReceivePB, markFlag) == 1, "TCPReceivePB ABI"); +static_assert(offsetof(TCPReceivePB, urgentFlag) == 2, "TCPReceivePB ABI"); +static_assert(sizeof(TCPStatusPB) == 70, "TCPStatusPB ABI"); +static_assert(sizeof(TCPiopb) == 102, "TCPiopb ABI"); static_assert(offsetof(GetAddrParamBlock, ourAddress) == 28, "GetAddrParamBlock ABI"); static_assert(sizeof(wdsEntry) == 6, "wdsEntry ABI"); diff --git a/tests/mactcp_abi.cpp b/tests/mactcp_abi.cpp index c53c3a2a..49152f4c 100644 --- a/tests/mactcp_abi.cpp +++ b/tests/mactcp_abi.cpp @@ -12,9 +12,16 @@ * can slip through. These per-field offsets catch that. * * Retro68, against Apple's Universal Interfaces MacTCP.h: - * are the numbers we took from the MacTCP Programmer's Guide - * actually correct? This is the ground truth, and it is the - * half that settles the VERIFY markers in the YAML. + * are those numbers actually correct? This is the ground truth. + * + * The expected values below have been confirmed field by field against + * Universal Interfaces 3.4.2, by compiling Apple's MacTCP.h for a + * 4-byte-pointer target with 2-byte packing (which is what mac68k + * alignment amounts to for these types) and reading back the offsets + * the compiler computed. Two blocks did *not* match the MacTCP + * Programmer's Guide, and both are called out where they appear below: + * TCPReceivePB's field order, and TCPStatusPB's connStatPtr, whose + * omission also made TCPiopb 98 bytes instead of 102. * * IMPORTANT: the Retro68 half only means something when it compiles * against *Apple's* headers. Retro68 generates its CIncludes from the @@ -32,7 +39,7 @@ * that way, and every parameter block field it touches agrees with the * names used here. * - * To settle the VERIFY markers: + * To re-confirm after a change: * * ./tests --gtest_filter=MacTCPABI.DumpLayout # native, ours * ... same test built as the Retro68 application, run under @@ -42,9 +49,9 @@ * A compile failure on the Retro68 side is itself a result: it means a * field name in defs/MacTCP.yaml does not match Apple's. * - * The expected values below come from the MacTCP Programmer's Guide, - * not from the YAML, so that a transcription error in the YAML shows - * up as a failure here rather than being quietly mirrored. + * The expected values are written out here rather than derived from + * the YAML, so that a transcription error in the YAML shows up as a + * failure instead of being quietly mirrored. */ #include "gtest/gtest.h" @@ -113,7 +120,7 @@ using namespace Executor; X(csParam.open.optionCnt, 53) \ X(csParam.open.options, 54) \ X(csParam.open.userDataPtr, 94) \ - /* TCPSend -- VERIFY: position of the filler at 37 */ \ + /* TCPSend -- filler at 37, confirmed against Apple */ \ X(csParam.send.ulpTimeoutValue, 32) \ X(csParam.send.ulpTimeoutAction, 33) \ X(csParam.send.validityFlags, 34) \ @@ -123,16 +130,17 @@ using namespace Executor; X(csParam.send.sendFree, 42) \ X(csParam.send.sendLength, 46) \ X(csParam.send.userDataPtr, 48) \ - /* TCPRcv / TCPNoCopyRcv / TCPRcvBfrReturn \ - * VERIFY: relative order of urgentFlag and markFlag at 48/49 */ \ + /* TCPRcv / TCPNoCopyRcv / TCPRcvBfrReturn. markFlag and \ + * urgentFlag are at the front, not the tail: the Programmer's \ + * Guide is wrong about this block and Apple's header says so. */ \ X(csParam.receive.commandTimeoutValue, 32) \ - X(csParam.receive.rcvBuff, 34) \ - X(csParam.receive.rcvBuffLen, 38) \ - X(csParam.receive.rdsPtr, 40) \ - X(csParam.receive.rdsLength, 44) \ - X(csParam.receive.secondTimeStamp, 46) \ - X(csParam.receive.urgentFlag, 48) \ - X(csParam.receive.markFlag, 49) \ + X(csParam.receive.markFlag, 33) \ + X(csParam.receive.urgentFlag, 34) \ + X(csParam.receive.rcvBuff, 36) \ + X(csParam.receive.rcvBuffLen, 40) \ + X(csParam.receive.rdsPtr, 42) \ + X(csParam.receive.rdsLength, 46) \ + X(csParam.receive.secondTimeStamp, 48) \ X(csParam.receive.userDataPtr, 50) \ /* TCPClose */ \ X(csParam.close.ulpTimeoutValue, 32) \ @@ -141,7 +149,9 @@ using namespace Executor; X(csParam.close.userDataPtr, 36) \ /* TCPAbort */ \ X(csParam.abort.userDataPtr, 32) \ - /* TCPStatus -- VERIFY: the tail from srtt at 82 onwards */ \ + /* TCPStatus. connStatPtr at 94 is absent from the Guide's \ + * parameter table; leaving it out shortens the block to 66 and \ + * TCPiopb to 98. */ \ X(csParam.status.ulpTimeoutValue, 32) \ X(csParam.status.ulpTimeoutAction, 33) \ X(csParam.status.remoteHost, 38) \ @@ -161,9 +171,10 @@ using namespace Executor; X(csParam.status.congestionWindow, 74) \ X(csParam.status.rcvNext, 78) \ X(csParam.status.srtt, 82) \ - X(csParam.status.lastRtt, 86) \ + X(csParam.status.lastRTT, 86) \ X(csParam.status.sendMaxSegSize, 90) \ - X(csParam.status.userDataPtr, 94) + X(csParam.status.connStatPtr, 94) \ + X(csParam.status.userDataPtr, 98) #define MACTCP_GETADDR_FIELDS(X) \ X(ourAddress, 28) \ @@ -182,7 +193,7 @@ MACTCP_IOPB_FIELDS(ASSERT_IOPB_OFFSET) MACTCP_GETADDR_FIELDS(ASSERT_GETADDR_OFFSET) #undef ASSERT_GETADDR_OFFSET -static_assert(sizeof(TCPiopb) == 98, "TCPiopb size"); +static_assert(sizeof(TCPiopb) == 102, "TCPiopb size"); static_assert(sizeof(GetAddrParamBlock) == 36, "GetAddrParamBlock size"); static_assert(sizeof(wdsEntry) == 6, "wdsEntry size"); static_assert(sizeof(rdsEntry) == 6, "rdsEntry size"); @@ -211,15 +222,15 @@ TEST(MacTCPABI, GetAddrParamBlockFieldOffsets) TEST(MacTCPABI, StructSizes) { - EXPECT_EQ(98u, (unsigned)sizeof(TCPiopb)); + EXPECT_EQ(102u, (unsigned)sizeof(TCPiopb)); EXPECT_EQ(36u, (unsigned)sizeof(GetAddrParamBlock)); EXPECT_EQ(6u, (unsigned)sizeof(wdsEntry)); EXPECT_EQ(6u, (unsigned)sizeof(rdsEntry)); EXPECT_EQ(24u, (unsigned)sizeof(ICMPReport)); - /* csParam has to cover the largest member; open and status are - * tied at 66 bytes, which is what makes TCPiopb 98. */ - EXPECT_EQ(66u, (unsigned)(sizeof(TCPiopb) - 32)); + /* csParam covers its largest member, TCPStatusPB at 70 bytes, + * which is what makes TCPiopb 102. TCPOpenPB is 66. */ + EXPECT_EQ(70u, (unsigned)(sizeof(TCPiopb) - 32)); } /* Result codes are as much a part of the ABI as the layouts: an From af7c8f4f9bccb72d22a0697d876a5aaccc5307a7 Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Sun, 2 Aug 2026 14:26:17 -0700 Subject: [PATCH 7/8] Add the HTTP fetch that was phase 1's exit criterion A real HTTP/1.0 request goes out through TCPSend and a real response comes back: status line, headers, and a body larger than the application's receive buffer, so it takes a dozen TCPRcv calls to drain and the server's hangup is what ends the entity. That last part matters -- connectionClosing is the normal end of an HTTP/1.0 response, not an error, and an application has to treat it that way. The server is inside the test process rather than out on the internet. MacTCP has no resolver yet, so a real host would mean a hardcoded IP address that rots; a test needing egress cannot run in CI or offline; and the driver cannot tell the difference, since routing and DNS are not parts of it. Setting EXECUTOR_MACTCP_TEST_ADDR points the same exchange at a real server for anyone who wants to watch it work. --- tests/mactcp.cpp | 197 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/tests/mactcp.cpp b/tests/mactcp.cpp index 97de75d8..53eae32c 100644 --- a/tests/mactcp.cpp +++ b/tests/mactcp.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include +#include using namespace Executor; @@ -104,8 +107,10 @@ class LoopbackListener uint16_t port_ = 0; }; -/* Open a stream and connect it to the listener. Returns the refnum; - * the stream cookie is left in pb.tcpStream. */ +/* Open a stream and connect it. MacTCP has no resolver of its own -- + * names go through the DNR, a separate code resource and phase 3 work + * -- so every connect here is by numeric address, exactly as an + * application of the era would have done it. */ void createAndConnect(INTEGER refnum, TCPiopb& pb, Ptr rcvBuff, uint32_t rcvBuffLen, uint16_t port) { @@ -363,3 +368,191 @@ TEST(MacTCP, PeerCloseIsReportedAsConnectionClosing) DisposePtr(appBuff); DisposePtr(rcvBuff); } + +/* ---- a real HTTP/1.0 fetch ---------------------------------------- + * The phase 1 exit criterion was "a hand-rolled port-80 fetch works". + * This is that exchange, driven entirely through the .IPP driver: + * a genuine HTTP request goes out, a genuine response with headers and + * a body larger than the application's receive buffer comes back, and + * the server closes the connection to signal the end of the entity -- + * HTTP/1.0 semantics, which is what a 90s Mac client would have spoken. + * + * It talks to a server inside the test process rather than out to the + * internet, for three reasons. MacTCP has no resolver yet, so a real + * host would have to be a hardcoded IP address that rots. A test that + * needs egress cannot run in CI or on a developer's laptop offline. + * And the driver cannot tell the difference: the same socket calls run + * either way, and the parts that would differ -- routing, DNS -- are + * not in the driver. Set EXECUTOR_MACTCP_TEST_ADDR (dotted quad) and + * optionally EXECUTOR_MACTCP_TEST_PORT to point the same exchange at a + * real server. + */ + +namespace +{ + +/* Read from a stream until the peer hangs up, appending to `out`. + * Returns the OSErr that ended the loop. */ +OSErr drainStream(INTEGER refnum, TCPiopb& pb, GUEST stream, + Ptr appBuff, uint16_t appBuffLen, std::string& out) +{ + for(;;) + { + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.receive.rcvBuff = appBuff; + pb.csParam.receive.rcvBuffLen = appBuffLen; + pb.csParam.receive.commandTimeoutValue = 10; + + OSErr err = control(refnum, pb, TCPRcv); + if(err != noErr) + return err; + + uint16_t got = pb.csParam.receive.rcvBuffLen; + if(got == 0) + return noErr; + out.append((const char *)appBuff, got); + } +} + +} /* namespace */ + +TEST(MacTCP, HttpFetch) +{ + INTEGER refnum = openIPP(); + + /* A body deliberately larger than the receive buffer below, so the + * response cannot arrive in one TCPRcv. + * + * Keep it comfortably inside the socket buffers. Driver calls here + * are synchronous and the server side is the same thread, so the + * server writes its whole response before the driver reads any of + * it; a response too large to sit in the kernel buffers would block + * the send and deadlock the test. Growing this much beyond a few + * tens of KB needs a thread or a poll loop on the server side. */ + std::string body; + for(int i = 0; body.size() < 10000; ++i) + body += "line " + std::to_string(i) + " of the response body\n"; + + std::string response = + "HTTP/1.0 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: " + std::to_string(body.size()) + "\r\n" + "\r\n" + body; + + uint32_t host = 0x7F000001; + uint16_t port = 0; + std::unique_ptr listener; + + if(const char *addr = getenv("EXECUTOR_MACTCP_TEST_ADDR")) + { + struct in_addr in; + ASSERT_EQ(1, inet_pton(AF_INET, addr, &in)) << "bad test address"; + host = ntohl(in.s_addr); + port = 80; + if(const char *p = getenv("EXECUTOR_MACTCP_TEST_PORT")) + port = (uint16_t)atoi(p); + } + else + { + listener = std::make_unique(); + port = listener->port(); + } + + const uint32_t kRcvBuffLen = 8192; + Ptr rcvBuff = guestBuffer(kRcvBuffLen); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + pb.csParam.create.rcvBuff = rcvBuff; + pb.csParam.create.rcvBuffLen = kRcvBuffLen; + ASSERT_EQ(noErr, control(refnum, pb, TCPCreate)); + auto stream = pb.tcpStream; + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.open.remoteHost = host; + pb.csParam.open.remotePort = port; + pb.csParam.open.ulpTimeoutValue = 10; + ASSERT_EQ(noErr, control(refnum, pb, TCPActiveOpen)) + << "TCPActiveOpen failed"; + + /* --- send the request --------------------------------------- */ + const char request[] = + "GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + + Ptr reqBuff = guestBuffer(sizeof(request)); + std::memcpy(reqBuff, request, sizeof(request) - 1); + Ptr wdsMem = guestBuffer(sizeof(wdsEntry) * 2); + auto *wds = (wdsEntry *)wdsMem; + wds[0].length = sizeof(request) - 1; + wds[0].ptr = reqBuff; + wds[1].length = 0; + wds[1].ptr = nullptr; + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.send.wdsPtr = wdsMem; + pb.csParam.send.pushFlag = 1; + pb.csParam.send.ulpTimeoutValue = 10; + ASSERT_EQ(noErr, control(refnum, pb, TCPSend)); + + /* --- the server side ----------------------------------------- */ + if(listener) + { + int peer = listener->accepted(); + ASSERT_GE(peer, 0); + + char req[512] = {}; + ssize_t n = recv(peer, req, sizeof(req) - 1, 0); + ASSERT_GT(n, 0); + EXPECT_NE(nullptr, std::strstr(req, "GET / HTTP/1.0")) + << "server did not receive a well-formed request"; + + ASSERT_EQ((ssize_t)response.size(), + send(peer, response.data(), response.size(), 0)); + shutdown(peer, SHUT_WR); /* HTTP/1.0: close ends the entity */ + } + + /* --- read the response --------------------------------------- */ + Ptr appBuff = guestBuffer(1024); + std::string got; + OSErr err = drainStream(refnum, pb, stream, appBuff, 1024, got); + + /* A clean server hangup surfaces as connectionClosing, which is + * the normal end of an HTTP/1.0 response, not a failure. */ + EXPECT_TRUE(err == noErr || err == connectionClosing) + << "unexpected error draining response: " << err; + + ASSERT_FALSE(got.empty()) << "no response received"; + EXPECT_EQ(0u, got.rfind("HTTP/1.", 0)) << "not an HTTP response"; + + size_t hdrEnd = got.find("\r\n\r\n"); + ASSERT_NE(std::string::npos, hdrEnd) << "no header terminator"; + + std::string gotBody = got.substr(hdrEnd + 4); + if(!listener) + { + /* Against a real server we cannot predict the body, so just + * insist we got a plausible amount of it. */ + EXPECT_GT(got.size(), 16u); + } + else + { + EXPECT_EQ(body.size(), gotBody.size()) + << "body truncated: needed several TCPRcv calls"; + EXPECT_EQ(body, gotBody); + } + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + control(refnum, pb, TCPClose); + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPRelease)); + + DisposePtr(appBuff); + DisposePtr(wdsMem); + DisposePtr(reqBuff); + DisposePtr(rcvBuff); +} From 2c3d050a8f41fd908379b5498fef6dbe72dc4c12 Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Mon, 3 Aug 2026 15:40:12 -0700 Subject: [PATCH 8/8] Update multiversal submodule --- multiversal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multiversal b/multiversal index 6832f1b2..fdd79600 160000 --- a/multiversal +++ b/multiversal @@ -1 +1 @@ -Subproject commit 6832f1b2aee838777d58951d05340a532e3be683 +Subproject commit fdd796001bcabf83990b7dc865dff5325ec2cec7