The managed netpoller — hand-owning the ten runtime_poll* contracts

STATUS: DESIGN — PROPOSED (2026-08-12, lane netpoll-design). Nothing in this document is ratified. Every decision below is a proposal with a recommendation; §9 collects the ones that need a coordinator ruling before any implementation lane starts. Commissioned by the board’s sockaddr RESOLVED note (BOARD-next-validation-candidates.md, “RESOLVED 2026-08-11 (lane L10)”: “that is a design arc with a deadline/unblock story to settle, not a wrapper repair — it wants its own DESIGN doc and a coordinator ruling before anyone starts”) and the matching L10 correction in LANES.md §L10. Companions: src/core/internal/poll/fd_poll_runtime.cs (the ten stubs and all their callers), src/core/internal/poll/windows/fd_windows.cs (execIO, the contract’s driver), src/core/runtime/netpoll.cs + src/core/runtime/windows/netpoll_windows.cs (the converted runtime counterparts this design deliberately leaves dead), src/core/golib/ж.cs (the address model the submit seam prices against), and the precedent files cited inline. Written against the corpus at 0133b6aa7 (2026-08-12), Go 1.23.1.


1. The wall, measured — what stops net.Listen today

With L10’s sockaddr fixes merged, syscall.Bind succeeds and net.Listen on Windows walks on to this, the exact stack the board banked:

System.NotImplementedException: runtime_pollServerInit: external (assembly or cgo) function is not implemented
  at internal/poll.runtime_pollServerInit  (PartialStubGenerator stub)
  at internal\poll.pollDesc.init -> internal\poll.FD.Init  ... fd_poll_runtime.cs:48
  at net.netFD.init -> net.listenStream                    ... sock_posix.cs:216
  at net.Listen                                            ... dial.cs:933

internal/poll declares ten bodyless //go:linkname entry points into the runtime’s network poller (fd_poll_runtime.cs:18–36). The converter emits each as a bodyless partial method; with no body provided, the PartialStubGenerator fills them with throwing stubs, and the first pollable FD.Init — every socket net creates (net/windows/fd_windows.cs:59 passes pollable: true; os passes false for every file/pipe/console, os/windows/file_windows.cs:83) — dies in serverInit.Do(runtime_pollServerInit).

The counterparts EXIST in the converted runtime — runtime/netpoll.cs:217 carries poll_runtime_pollServerInit with its //go:linkname comment intact, and all nine others are beside it — but nothing wires a linkname across assemblies, and §3 shows that wiring them would not be sufficient: the bodies bottom out in the runtime scheduler, which does not exist under the CLR. The honest remedy is the managed-API-boundary pattern this repo has used four times already (§3.3): hand-own the ten CONTRACTS in internal/poll against .NET’s own completion machinery, and leave the runtime’s poller exactly as converted — dead.

This is a Windows-first design. The corpus’s compile target is $(GoTargetOS)=windows; the Linux internal/poll is a readiness-model consumer (fd_unix.go retries the syscall after pd.wait) rather than a completion-model one, and gets its own design when the Linux corpus compiles (§8).

2. The contract inventory — ten functions, every caller

All ten stubs live in fd_poll_runtime.cs (build tag unix || windows || wasip1 — the file is FLAT in the L3 layout, shared by every GOOS). The callers below are the complete set inside internal/poll; nothing else in the corpus calls them (the runtime’s own poll_runtime_* twins are unreferenced exports).

# Stub (fd_poll_runtime.cs) Contract (from runtime/netpoll.cs) Called by Reached from
1 runtime_pollServerInit() (:20) Initialize the poller, once (:217–232, netpollGenericInit) pollDesc.init via serverInit.Do (:48) first pollable FD.Initnet.Listen, net.Dial, accepted conns
2 runtime_pollOpen(fd) (uintptr, nint) (:22) Register fd; return an opaque nonzero ctx or errno (:251–284) pollDesc.init (:49) same
3 runtime_pollClose(ctx) (:24) Unregister; only legal after unblock (:292–308) pollDesc.close (:61) FD.destroy (last decref)
4 runtime_pollWait(ctx, mode) nint (:26) Block until IO-ready in mode, or return the deadline/closing code (:355–374) pollDesc.waitwaitRead/waitWrite (:92–106) execIO after ERROR_IO_PENDING (fd_windows.cs:188)
5 runtime_pollWaitCanceled(ctx, mode) (:28) Block until IO-ready, IGNORING deadline/closing — Windows-only, after a CancelIoEx (:377–382) pollDesc.waitCanceled (:108–113) execIO cancel path (fd_windows.cs:219)
6 runtime_pollReset(ctx, mode) nint (:30) Clear consumed readiness; fail fast if closing/expired (:335–347) pollDesc.prepareprepareRead/prepareWrite (:73–87) execIO before every submit (fd_windows.cs:164)
7 runtime_pollSetDeadline(ctx, d, mode) (:32) Arm/replace/clear the read and/or write deadline; d is a RELATIVE ns duration (:385–467) setDeadlineImpl (:163–189) FD.SetDeadline/SetReadDeadline/SetWriteDeadlinenet.Conn deadlines
8 runtime_pollUnblock(ctx) (:34) Mark closing; wake both waiters with pollErrClosing; stop timers (:473–505) pollDesc.evict (:66–71) FD.Close (fd_windows.cs:406)
9 runtime_isPollServerDescriptor(fd) bool (:36) Report whether fd IS the poller’s own descriptor (:242–244) IsPollDescriptor (:203–205) test-only; linkname-pinned public surface (go.dev/issue/67401)
10 runtimeNano() int64 (:18, //go:linkname runtimeNano runtime.nanotime) Monotonic ns, arbitrary epoch (declared for the shared file; no Windows-path caller today)

Three contract facts that pin the implementation:

The driver. execIO (fd_windows.cs:156–232) is the loop every network operation runs: prepare (reset) → submit(o) — an overlapped WSA call handed &o.o — → on ERROR_IO_PENDING, wait → on success, harvest via windows.WSAGetOverlappedResult(…, wait: false); on deadline/close, CancelIoEx(fd, &o.o)waitCanceled → harvest, mapping ERROR_OPERATION_ABORTED back to the deadline/close error. The submit lambdas across fd_windows.cs + sendfile_windows.cs cover, by native entry point: WSARecv (Read :459, RawRead :1236), WSARecvFrom (ReadFrom family :615/:653/:691), WSASend (Write :757, Writev :885, RawWrite), WSASendto (WriteTo family :912–:1004), ConnectEx (:1025), AcceptEx (:1041, paired with GetAcceptExSockaddrs in net/windows/fd_windows.cs:253), WSARecvMsg/WSASendMsg (ReadMsg/WriteMsg families :1352–:1463+), and TransmitFile (sendfile_windows.cs:71). This census is what §4.3 stages.

3. The wall behind the wall — why this design routes AROUND the runtime, not through it

3.1 The asmstdcall wall is real…

Wiring stub #1 to the runtime’s poll_runtime_pollServerInit reaches netpollGenericInitnetpollinitstdcall4(_CreateIoCompletionPort, …) (runtime/windows/netpoll_windows.cs:106) → asmstdcall, a PartialStubGenerator stub: the runtime’s syscall path is hand-written assembly Go, and go2cs has never implemented it — the whole corpus dispatches through the hand-owned managed trampoline instead (syscall/windows/dll_windows.cs:93–122, an unmanaged function-pointer calli switch). That single call could be patched — CreateIoCompletionPort has a working converted wrapper (zsyscall_windows.cs:602) — which is precisely why the wall must be named as more than asmstdcall.

3.2 …but it is the SHALLOW wall. The deep wall is the scheduler.

Behind netpollinit, the runtime bodies consume, in order of appearance (runtime/netpoll.cs): lockInit/lock/unlock on runtime mutexes with lock-rank bookkeeping (:223–225), pollcache.alloc over persistentalloc non-GC memory (:736), the timer struct and its modify/stop engine (:420–445) — the runtime timer subsystem entire — gopark with a commit callback (:605), goready (:568), g-pointer CAS protocols storing goroutine identity in a uintptr (:557), and the eface/_type reinterpret of makeArg (:757–768). Every one of those is an organ of the Go scheduler. None exists under the CLR, and none CAN exist: a go2cs goroutine is a managed thread the CLR schedules (runtime/managed_impl.cs, “Honest divergences”), so there is no g to park, no P to hand a ready g to, and no sysmon to pump netpoll(delta).

That last point is the decisive one. Go’s poller is only half an API — the other half (netpoll(delta), netpollBreak, netpollready, netpollAnyWaiters; the platform interface listed at runtime/netpoll.cs:15–40) is called by the scheduler itself from findrunnable and sysmon. Under go2cs nothing would ever pump it: a perfectly-wired conversion would initialize an IOCP and then block forever, because the thread Go dedicates to draining it IS the scheduler. Emulating that means writing a scheduler. The ten-contract boundary is the only cut through this subsystem that does not drag the scheduler across.

3.3 The doctrine, and the four precedents

runtime/managed_impl.cs states the fork this repo already took, verbatim: “where a Go mechanism has no managed counterpart but its PUBLIC CONTRACT does, reimplement the CONTRACT at the API boundary and never emulate the mechanism… Everything below these entry points stays auto-converted and simply becomes unreachable.” Four landings prove it at increasing depth:

  1. sync’s Mutex/RWMutex/WaitGroup (sync/mutex.cs) — the runtime sleeping semaphore reimplemented on SemaphoreSlim/monitors; the co-designed starvation handoff explicitly NOT emulated.
  2. internal/poll’s own runtime_Semacquire/runtime_Semrelease (internal/poll/runtime_sema_impl.cs) — the EXACT mechanical shape this design reuses: an _impl.cs beside the converted package, [module: go.GoManualConversion], bodies for the bodyless partials (which suppresses the PartialStubGenerator stubs), keyed by ж-box identity where address identity is the contract.
  3. runtime’s process-control surface (runtime/managed_impl.cs) — ReadMemStats, the traceback/Callers surface, GC entry points: contracts on CLR primitives, converted machinery below left dead.
  4. syscall’s runtime-provided primitives (syscall/syscall_impl.cs, syscall/windows/dll_windows.cs) — Exit/Getpagesize and the entire Syscall/SyscallN trampoline: the runtime’s assembly replaced by a managed dispatcher.

Durability of the cut. The ten linknames are one of Go’s most compatibility-pinned internal surfaces — IsPollDescriptor carries the hall-of-shame linkname notice (“Do not remove or change the type signature”, go.dev/issue/67401, fd_poll_runtime.cs:194–202), and Go 1.23 itself rewrote netpoll_windows.go’s internals (source-tagged completion keys, timer sources — runtime/windows/netpoll_windows.cs:18–66) while the ten crossed unchanged. A seam that Go’s own churn respects is a seam worth owning.

4. The design — the ten contracts on .NET’s completion machinery

4.1 The managed pollDesc

A new hand-owned file, src/core/internal/poll/windows/runtime_netpoll_impl.cs (per-GOOS folder; rides the existing <Compile Include="$(GoTargetOS)/*.cs" /> glob, internal.poll.csproj:145 — no csproj change), carrying [module: go.GoManualConversion] per the marker rules, provides the ten partial bodies over:

sealed class ManagedPollDesc {
    object gate;                    // ONE lock; §5 explains why lock-free is not owed
    bool closing;                   // pollUnblock ran (sticky for the desc's lifetime)
    ModeState r, w;                 // per-mode:
      bool ready;                   //   an IO completion arrived and is unconsumed
      bool expired;                 //   deadline passed and not since re-set (STICKY, §5)
      long generation;              //   invalidates stale timer callbacks (Go's rseq/wseq)
      Timer? timer;                 //   armed only while a deadline is pending
    nuint fd;                       // for isPollServerDescriptor bookkeeping only
}
static ConcurrentDictionary<uintptr, ManagedPollDesc> table;   // ctx token -> desc
static long nextToken;                                          // tokens start at 1; 0 = "no ctx"

Contract bodies, mapped one-to-one:

The completion callback (§4.2) is the only writer of ready: under the lock, ready = true, PulseAll. Timeout and close wake waiters WITHOUT setting ready — the two-layer separation (wake vs readiness) is the load-bearing structure Go encodes in pdReady-vs-pdNil-wake, and it is what makes waitCanceled’s ignore-errors loop correct.

Why one Monitor instead of Go’s lock-free CAS protocol. Go splits pollDesc state across atomics (rg/wg, atomicInfo) because netpollcheckerr runs where the pd lock cannot be taken and netpollblock parks through gopark’s publication protocol (runtime/netpoll.cs:83–95, 556–613). Neither constraint survives the boundary: every managed caller is an ordinary blocked thread, and the single-waiter-per-mode contract bounds convoying to one reader + one writer + one timer callback per desc. A Monitor is the honest primitive; the CAS choreography would be emulation of a mechanism whose reason evaporated. (Precedent: runtime_sema_impl.cs made the same reduction for the runtime semaphore — “a bucket is just a count plus a FIFO waiter queue.”)

4.2 Completion delivery — two candidate mechanisms

The poller’s one irreducible job on Windows: when the kernel completes an overlapped operation that internal/poll submitted, set ready on the right desc’s mode. Go does it by owning an IOCP and draining it from the scheduler (GetQueuedCompletionStatusEx in netpoll()); the completion routes back via “the overlapped is the first field of operation pointer arithmetic (fd_windows.cs:72–74, runtime/windows/netpoll_windows.cs:58–66). The managed replacement has two candidate shapes:

(a) ThreadPoolBoundHandle — the CLR’s own IOCP (RECOMMENDED). At pollOpen, bind the socket handle (ThreadPoolBoundHandle.BindHandle(SafeHandle)); per operation record (§4.3), allocate the NativeOverlapped from a reusable PreAllocatedOverlapped whose callback closes over the record → desc → mode. The CLR’s IO thread pool dequeues the completion and runs the callback with (errorCode, numBytes, NativeOverlapped*); the callback signals ready. This is “.NET’s own completion-port machinery” in the board’s words, literally: no poller thread of ours, no shutdown/teardown story, no key-routing table addressed by raw pointers, AOT-compatible, and the same engine .NET’s own Socket rides. runtime_pollServerInit reduces to state initialization; runtime_isPollServerDescriptor returns false for every fd — there IS no exposed poll-server descriptor (the contract’s only consumer is the test-only IsPollDescriptor; a Go program cannot legitimately hold the poller’s fd on Windows anyway).

(b) Own IOCP + a dedicated managed poller thread. runtime_pollServerInit creates the port (the converted syscall.CreateIoCompletionPort wrapper works — zsyscall_windows.cs:602); pollOpen associates with a completion key; a background thread blocks in GetQueuedCompletionStatus(Ex) and routes by key. Closer to Go’s shape; isPollServerDescriptor gets a real answer. Costs: a thread whose lifecycle nobody owns (host shutdown, test-host isolation), a NativeOverlapped-to-record routing table keyed by raw native addresses, and hand-rolled dequeue marshalling — all machinery (a) gets from the CLR for free.

Recommendation: (a), with (b) documented as the fallback if a real BindHandle constraint surfaces (e.g., a handle the CLR refuses to bind). Nothing in the contract layer changes between them — the choice is confined to pollServerInit/pollOpen and the callback’s plumbing — so a later swap is not a redesign. OQ1.

RESOLVED at S1 (2026-08-13): mechanism (a) holds. ThreadPoolBoundHandle.BindHandle accepts a Go-created socket handle against a real kernel — net.Listen completes and NetListenSmoke matches go run byte for byte. The fallback to (b) is not needed.

AMENDED at S2 (2026-08-14): the bind moves from pollOpen to the FIRST SUBMIT (lazy), inside the §4.3 record machinery. This is the plumbing latitude this paragraph already grants (“confined to pollServerInit/pollOpen and the callback’s plumbing”), taken for a reason S1 measured rather than a preference. Go’s poller REJECTS completions it does not own: pollOperationFromOverlappedEntry (runtime/netpoll_windows.go) checks the completion key against the pollDesc pointer packed into it and returns nil on mismatch, citing go.dev/issue/58870 — the issue internal/poll’s own TestWSASocketConflict regression-guards. The CLR’s ThreadPoolBoundHandle offers no equivalent: its callback resolves state FROM the NativeOverlapped it allocated, so a foreign overlapped arriving at that port is MISREAD rather than ignored.

Binding at pollOpen therefore made every pollable socket eligible to receive a foreign completion, and internal/poll has a live path that produces one: FD.WSAIoctl (windows/sockopt_windows.cs:11) bypasses execIO entirely and hands the kernel the CALLER’s syscall.Overlapped — which, being an all-scalar struct in a STANDARD box, really does pin and really does reach the kernel. Binding lazily removes the hazard structurally instead of detecting it: a socket that only ever sees foreign overlapped IO is never associated with the CLR’s port, so its completions signal the caller’s own event exactly as on an unregistered socket, and a socket doing our IO is bound at its first submit, after which every operation on it carries a CLR-allocated overlapped. The residual — one socket doing BOTH — is not reachable in the corpus: a census of every FD.WSAIoctl caller (net/windows/fd_windows.cs:177 SIO_TCP_INITIAL_RTO, net/windows/tcpsockopt_windows.cs:124 SIO_KEEPALIVE_VALS) shows both pass a nil overlapped, so no production path issues an asynchronous foreign operation at all.

One converted behavior interacts here and is kept: FD.Init enables SetFileCompletionNotificationModes(FILE_SKIP_COMPLETION_PORT_ON_SUCCESS) for TCP/UDP when safe and sets skipSyncNotif (fd_windows.cs:333–345), so synchronously-completing operations post NO completion packet and execIO returns without waiting (:171–177). Under (a) that is supported (it is how .NET’s own sockets run) but obligates the record lifecycle to a “submit may retire with no callback” path — FreeNativeOverlapped on the sync-return branch, re-arm on the next submit. The smaller-state-space alternative — suppress the mode and let every completion post — is OQ5.

4.3 The submit seam — the second wall, and the WSA mirror family

Making pollWait wake up is HALF the arc. The other half is that the overlapped submissions execIO issues must actually reach the kernel and complete into memory the managed side can read. Today they cannot, and the board’s syscall STRUCT-PASSING census (BOARD-next-validation-candidates.md §”Open — the syscall STRUCT-PASSING seam”) predicted exactly this: net is the package that forces the remaining members. Async adds a dimension the census’s synchronous members never had. Three distinct sub-walls, priced separately:

(1) The native-layout wall (the established class, new members). syscall.WSABuf is {uint32 Len; ж<byte> Buf} (types_windows.cs:555–558) — a managed reference where native WSABUF wants a raw CHAR*. Every submit lambda passes &o.buf / &o.bufs[0] / &o.msg (WSAMsg — worse: it embeds a ж<WSABuf> Buffers pointer and a control buffer). Same class as Timezoneinformation/win32finddata1/RawSockaddrInet4, same remedy: blittable [StructLayout(Sequential)] mirrors with an explicit field-for-field copy at the boundary. AcceptEx’s output buffer is the class’s decode-side: acceptOne hands it a slice<RawSockaddrAny> reinterpreted as bytes (fd_windows.cs:1041), whose managed layout cannot hold the native sockaddr block — the mirror must be a NATIVE staging buffer, decoded at harvest with the L10 mirror helpers (syscall/windows/syscall_windows_impl.cs already owns the RawSockaddrInet4/6 ↔ native translation both directions).

(2) The lifetime wall (what async ADDS to the class). The census’s fixed members marshal with a mirror that is “a LOCAL at the call site… trivially stable for exactly that long” (syscall_windows_impl.cs header). An overlapped operation breaks that premise twice over:

The remedy: a per-(FD, mode) operation RECORD owning native-lifetime state. The hand-owned WSA wrappers keep a table ж<Overlapped> → OpRecord. The key works because golib pointer equality for field-reference boxes compares (source box, field identity) — ж.cs:63–70 documents that this exact property exists to serve “the address-keyed runtime semaphores in the hand-owned sync/internal-poll implementations” — and each FD has exactly one read op (rop) and one write op (wop) for its lifetime (fd_windows.cs:242–244, 361–366), so &o.o minted at any call site of either resolves to the same record. The record owns: the PreAllocatedOverlapped/NativeOverlapped (mechanism (a)) or a NativeMemory.Alloc‘d OVERLAPPED (mechanism (b)); the native WSABUF array/WSAMSG/sockaddr staging blocks; the pins (ж<byte> element boxes hold their backing-array pins for the BOX’s lifetime, and the record holds the boxes, covering the op’s whole flight); and the completion results (errorCode, qty) the callback deposits. WSAGetOverlappedResult’s hand-own answers from the record; CancelIoEx’s hand-own targets the record’s one true native address. Out-parameters (&o.qty, &o.flags, rsan) are marshalled through call-local natives and copied back after the call — never through interior pins — per the mirror-is-a-local doctrine, which async does NOT break for out-params (they are written only during the synchronous portion or at harvest, both call-bounded).

(3) The displacement mechanism. The wrappers to hand-own are converter-generated with real bodies (zsyscall_windows.cs:1516 for WSARecv, etc.), so unlike the ten bodyless stubs they must be DISPLACED, not merely supplied. The established mechanism is manualConversionFuncs (src/go2cs/manualTypeOperations.go:92, goosWindows-scoped) — a data-only converter map entry per function that turns the generated body into a placeholder, with the hand-own in an _impl.cs beside it; the sockaddr family (:499–508) is the freshest precedent. The affected converted files regenerate once (the A/B footprint is exactly those files). OQ2 confirms the mechanism; OQ3 confirms the staged scope:

Stage Wrappers (owning package) Unlocks
S1–S2 (TCP core) WSARecv, WSASend, AcceptEx, GetAcceptExSockaddrs, ConnectEx, CancelIoEx (syscall); WSAGetOverlappedResult (internal/syscall/windows, wrapper at windows/zsyscall_windows.cs:521) listen/accept/dial/read/write/deadlines — every TCP consumer row
S3 (UDP) WSARecvFrom, WSASendto (syscall); WSASendtoInet4/6, WSARecvMsg, WSASendMsg (internal/syscall/windows) UDP-shaped suites, ReadFrom/WriteTo/ReadMsg paths
deferred until reached TransmitFile (sendfile_windows.cs:71) net’s sendfile fast path (has a non-sendfile fallback)

Per the board’s standing ruling for this class — “Do them when a suite reaches them, not speculatively” — nothing outside a stage’s gate lands with that stage. LoadConnectEx’s WSAIoctl function-pointer lookup and the socket-creation path (WSASocket, bind, listen, setsockopt) are synchronous and already work under the existing dispatcher + L10 mirrors; they are NOT part of this arc’s surface.

4.4 The submit seam, specified — findings from S2a that the implementation should not re-derive

Everything below was measured or read out of the corpus while landing S1/S2a. It is recorded because each item cost a non-obvious investigation and each one constrains the implementation.

The reference graph forces a PUSH, and golib is the only place the table can live. The waiter is internal/poll; the submissions are in syscall and internal/syscall/windows; internal/poll REFERENCES both, so a completion callback cannot call back into the poller. The three candidate resolutions and why two lose: a public seam on syscall_package would add a non-Go symbol to a PUBLISHED package’s API surface; reading the operation back from the overlapped (Go’s own trick — pollOperationFromOverlappedEntry casts the OVERLAPPED to the enclosing struct) is impossible here because ж<T>’s m_structFieldRef is private with no accessor, so a field-reference box cannot be walked back to its source object. What remains is a descriptor-keyed callback table in golib, the one assembly both sides see. Keep it platform-neutral to belong there: nuint handle → Action<nint mode>, plus an opaque object slot for the submitting package’s per-descriptor state (naming ThreadPoolBoundHandle in golib would drag Windows into it). The descriptor is the right key because it is the one identity both sides independently hold — the poller gets it in pollOpen, a wrapper gets it as its own first argument.

The golib table has two MEASURED requirements that its obvious implementation does not meet (S2b, prototyped as GoAsyncIO with 11 GolibTests; the code is not banked — §4.5 — but these are priced findings the implementation must honor, not advice). (1) Create-exactly-once is load-bearing, and ConcurrentDictionary.GetOrAdd does not provide it. Its factory may run on several threads with only one result kept; a contention test built 10 operation records where the contract wants 1, and each record owns a PreAllocatedOverlapped plus native staging buffers, so the nine discarded ones are a native leak with no owner. The per-descriptor slot has the same requirement for a harder reason — that object is the ThreadPoolBoundHandle association, and associating one socket twice is a kernel error. Use Lazy<T> with ExecutionAndPublication, or an explicit lock over the entry; never bare GetOrAdd. (2) The readiness sink must be REPLACEABLE per descriptor, because the kernel reissues descriptor numbers after a close: a registration that refused to overwrite would leave a stale sink waking a pollDesc that no longer owns the fd. A signal for an unregistered descriptor must be a silent no-op — it runs on a CLR IO thread-pool thread, where an escaping exception ends the process, and a completion racing a close is a race the contract permits.

The record key is verified, not assumed. ConcurrentDictionary<ж<Overlapped>, OpRecord> is sound: ж<T>.Equals compares SameSource(source1, source2) && fieldId1.Equals(fieldId2) for a struct-field reference, and GetHashCode returns SourceIdentityHash(source) — coarser than Equals (every field of one operation hashes alike) but consistent with it, which is all a dictionary requires. So Ꮡo.of(operation.Ꮡo) minted at execIO’s three separate call sites — submit, CancelIoEx, harvest — resolves to ONE record. ж.cs’s own comment says this property exists to serve “the address-keyed runtime semaphores in the hand-owned sync/internal-poll implementations”, so the arc is reusing a guarantee the corpus already depends on rather than leaning on an accident.

Why &o.o genuinely cannot be handed to the kernel, confirmed at the source. Overlapped is all-scalar (Internal, InternalHigh, Offset, OffsetHigh, HEvent) and so is blittable on its own — but that is not what decides it. EnsureStableAddress pins PinnableStorage, and for a struct-field reference that recurses to the CONTAINER’s storage: the ж<operation> box’s m_slot, which is null, because operation holds managed references (ж<FD> fd, WSABuf.Buf, slice<WSABuf> bufs, ж<RawSockaddrAny> rsa) and the value constructor allocates a pinnable slot only for a T free of them. So the address is transient exactly as §4.3 claims. Note the contrast that makes the FD.WSAIoctl hazard real: a caller’s own var ov syscall.Overlapped is a STANDARD box of a blittable T, so it does get a slot, does pin, and does reach the kernel.

Wrapper inventory, corrected. ConnectEx is already hand-owned by the L10 sockaddr lane (syscall/windows/syscall_windows_impl.cs:363, placeholder at syscall_windows.cs:1110) and today passes Ꮡoverlapped straight through to the generated connectEx — it must be EXTENDED to use the record’s native overlapped, not newly displaced, and it needs no new manualConversionFuncs entry. The genuinely new entries are WSARecv, WSASend, AcceptEx, GetAcceptExSockaddrs, CancelIoEx (all "syscall", goosWindows) and WSAGetOverlappedResult (a new "internal/syscall/windows" key). All six generated bodies share one shape — marshal through Syscall/Syscall9, test r1 against socket_error (or 0 for the BOOL-returning ones), errnoErr(e1) — so the hand-owns should reproduce that error handling verbatim rather than inventing one.

Mode is known from the wrapper, not from the operation. The callback must name a mode, and the record cannot read operation.mode (previous point). It does not need to: WSARecv/WSARecvFrom/ AcceptEx/WSARecvMsg are always the READ operation and WSASend/WSASendto/ConnectEx/ WSASendMsg always the WRITE one, because each FD has exactly one rop and one wop for its lifetime. The wrapper knows which it is by being itself.

Where the bind goes. First submit, inside the record machinery, per the §4.2 amendment — not pollOpen. AllocateNativeOverlapped must come from that same binding, and pollClose disposes it through the golib table’s opaque slot before internal/poll closes the socket.

AllowUnsafeBlocks splits between the two displacement packages, and the difference is owed work. syscall.csproj already emits true, so its mirrors and NativeOverlapped* need no marker. internal/syscall/windows emits false, so its WSAGetOverlappedResult hand-own must carry [module: go.GoRequiresUnsafe] if it touches a pointer type — and the regenerated .csproj flipping to true is then part of that stage’s intended A/B footprint, not drift. (The marker scan reads the package directory plus its per-GOOS folders, so a hand-own in windows/ is seen; see A hand-owned file can declare that it needs /unsafe in ConversionStrategies-Reference.)

4.5 The submit seam, MEASURED — what S2b settled, and the one hole §4.4 did not know about

S2b took the accept decode (§7’s blocker, now closed) and then priced the displacement itself against the corpus rather than against the plan. Three of §4.4’s assumptions are now VERIFIED rather than assumed, one golib design defect was found and fixed in a prototype, and one item in the ruled spec turns out not to be implementable as written. Everything below is measured; none of it should be re-derived.

VERIFIED — the record key really does resolve across execIO’s three call sites. §4.4 asserted this from ж.cs’s comment; it is now traced through the code. All three of execIO’s Ꮡo.of(operation.Ꮡo) sites (submit fd_windows.cs:190, CancelIoEx :212, harvest :220) mint a FRESH ж<Overlapped>, but each carries the same source object — the one ж<operation> Ꮡo parameter box, so SameSource’s ReferenceEquals holds — and the same identity token, because operation.Ꮡo is a static accessor method group and Delegate.Equals compares method+target. Equals returns true and GetHashCode returns SourceIdentityHash(Ꮡo) for all three. The bound on this is worth carrying: equality is SOURCE-POINTER identity, not value identity — three DISTINCT ж<operation> heap boxes over the same operation value would compare UNEQUAL. execIO never does that (the submit lambda receives the same box via submit(Ꮡo)), but any future call site that re-boxes would silently mint a second record.

VERIFIED — a user buffer’s pin is real, and the record must hold the BOX to keep it. (void*)Ꮡ(someByteSlice, 0) resolves through PinnableStorageCanonicalElement → the slice’s backing byte[]PinnedBuffer.PinOnly succeeds (blittable), so the address is genuinely pinned and cannot move. The pin is a GCHandle held by that one ж<byte> box, released only by PinnedBuffer’s finalizer — so the guarantee ends with the box, and Ꮡ(s, 0) mints a fresh temporary per call. For WSARecv/WSASend this is satisfied by the corpus already: the box lives in operation.buf.Buf (InitBuf, fd_windows.cs:95), a field of the operation inside the FD, so it outlives the flight. The record must still hold a reference to it rather than only to the address, or a future InitBuf on the same operation could drop the last reference to a box the kernel is still writing through.

VERIFIED — why &o.o cannot be handed to the kernel, and the exact fallback it takes. Reinterpret<T, TDst> for a reference-bearing T fails BOTH aliasing routes: ReinterpretAliasesStorage is false (IsReferenceOrContainsReferences<RawSockaddrAny> is true because array<T> is a struct over a T[], and LayoutCompatible then compares 2 source fields against 0 for byte), and TryPinnedReinterpret returns null because GCHandle.Alloc(…, Pinned) throws on a non-blittable array. It therefore lands on (ж<TDst>)(uintptr)box, which produces a box with a REAL interior address and no pin at all — stale the moment the fixed block exits. Note the compile-level fact underneath: golib’s fixed (void* ptr = &value.Value) over a managed T is legal C# with CS8500 suppressed project-wide, so nothing here is caught at build time.

FIXED IN PROTOTYPE — the golib seam, and a defect worth not repeating. §4.4 ruled the seam into golib; S2b prototyped it as GoAsyncIO (descriptor → readiness sink; an opaque per-descriptor state slot; an opaque per-operation slot keyed by the waiter’s pointer) with 11 GolibTests, all passing. The prototype is NOT banked, because dead public API in a published package is worse than a specification — but two things it measured belong here. (1) ConcurrentDictionary.GetOrAdd does not guarantee single execution of its factory: a contention test created 10 operation records where the contract wants 1, and each record owns a PreAllocatedOverlapped and native staging buffers, so the 9 discarded ones are a native leak with no owner. The per-descriptor state has the same requirement for a stronger reason — that object is the ThreadPoolBoundHandle association, and binding one socket twice is a kernel error. Use Lazy<T> with ExecutionAndPublication, or an explicit lock; do not use bare GetOrAdd. (2) The readiness sink must be REPLACEABLE per descriptor, because the kernel reissues descriptor numbers after a close and a stale sink would wake a pollDesc that no longer owns the fd.

SIMPLER THAN PLANNED — WSAGetOverlappedResult needs only an ADDRESS from the record. §4.4 had the hand-own answering from results the callback deposits, which would have required the record’s result fields to be readable across the package boundary and would have re-derived Windows’ own error mapping. It is enough to look the record up, take the native OVERLAPPED’s address, and call the REAL WSAGetOverlappedResult on it through the existing Syscall6 trampoline: the kernel wrote Internal/InternalHigh at completion time, so wait: false answers correctly after the CLR has already dequeued the packet, and — the reason this matters — it returns proper WSA error codes (WSAEMSGSIZE, ERROR_MORE_DATA), which is what execIO and net branch on. A callback’s Win32 errorCode is a DIFFERENT namespace (ERROR_NETNAME_DELETED where the suite expects WSAECONNRESET), so deriving the harvest from it would have been a quiet fidelity loss. Consequence: the golib contract between the two packages narrows to one property, the operation’s native address.

CENSUS — CancelIoEx has a second, NIL-overlapped caller. fd_windows.cs:403 calls CancelIoEx(fd.Sysfd, nil) to cancel ALL IO on a handle (the pipe/file close path), alongside :212’s per-operation form. The hand-own must pass a nil overlapped straight through rather than looking anything up, and must answer ERROR_NOT_FOUND — which execIO already tolerates — for a non-nil overlapped with no record, rather than falling back to 0 and cancelling everything on the socket.

⚠ THE HOLE, and why S2b stops here rather than landing a half-displaced set. GetAcceptExSockaddrs carries NO identity the seam can key on, and §4.4 did not notice because it reasoned about the overlapped family only. Its signature is (buf, rxdatalen, laddrlen, raddrlen, *lrsa, *lrsalen, *rrsa, *rrsalen) — no handle, no overlapped. Go recovers everything from buf, which is legitimate there because the accept buffer really is the caller’s [2]RawSockaddrAny and the kernel wrote native bytes into it. Under go2cs neither half survives: net passes Ꮡ(rawsa, 0).Reinterpret<RawSockaddrAny, byte>(), which per the measurement above is an UNPINNED native-address box over a managed RawSockaddrAny[] whose layout is not the native one — so the address is unusable as data, AND its identity is physical rather than structural, so two evaluations of the same expression are equal only if the GC did not move the array in between. It is therefore not usable as a table key either. AcceptEx (which does have the overlapped, and so can own a native staging buffer in its record) has no way to hand that buffer to GetAcceptExSockaddrs, and GetAcceptExSockaddrs has no way to ask for it.

Two shapes were viable. (a) A scoped handoff: AcceptEx’s hand-own parks its staging buffer in a slot that GetAcceptExSockaddrs consumes. Sound for the corpus’s only caller — net.netFD.accept runs FD.Accept and then GetAcceptExSockaddrs on one goroutine — but it is a NEW coupling between two wrappers Go keeps independent, and nothing in the type system sees it. (b) Transcribe at accept-completion into the managed rawsa, making GetAcceptExSockaddrs a purely managed parse. Note what is NOT a problem under either: the out-parameters. Ꮡlrsa/Ꮡrrsa are pointers to managed ж<RawSockaddrAny> VARIABLES, so a hand-own may write FRESH managed boxes into them; nothing in net requires them to point into buf, since the only thing done with them is .Sockaddr() — which is now hand-owned to decode from the managed image.

RULED (coordinator, 2026-08-14): shape (a), with three conditions that convert its one real cost — invisible coupling — into visible, loud machinery. Shape (b) is rejected on its own record: the transcriber must still find the caller’s array, so it is the same hole relocated, and its only escape hatch (having AcceptEx’s record capture the managed slice<RawSockaddrAny>) is self-refuted — AcceptEx receives the reinterpreted byte pointer and never sees the slice.

  1. Key the slot by GOROUTINE identity, not by a bare [ThreadStatic]. The identity source is the scheduler arc’s S1 registry, go.golib.Goroutine (src/core/golib/runtime/Goroutine.cs, merged), whose per-goroutine current-instance slot and Id are exactly this key; the implementing lane exposes an accessor if none is public yet. §7/OQ6’s ownership split explicitly sanctions netpoll ADOPTING scheduler identity at its own option, and this is that option exercised. Under the dedicated-thread executor a goroutine is one thread for life, so [ThreadStatic] would work today — goroutine-keying makes the coupling’s PREMISE explicit, survives any future executor change, and rides arc-owned identity rather than a runtime accident. Say so in the impl header: the scheduler arc is what makes this handoff sound, and that cross-arc dependency should be legible to whoever reads either arc next.
  2. Single-occupancy, consume-exactly-once, fail BY NAME. AcceptEx parking into an already occupied slot throws, naming both wrappers; GetAcceptExSockaddrs finding an empty slot throws likewise. A call sequence Go permits but the corpus never issues must fail LOUDLY, never be misread — the NetShareAdd declared-limit doctrine applied to a protocol instead of to a host capability.
  3. Document the coupling where it lives: the impl file’s header states the handoff, its goroutine-affinity premise, and the corpus census that bounds it — one caller, net.netFD.accept, doing accept-then-parse on one goroutine.

S2b’s own scope call under that ruling: the seam is ALL-OR-NOTHING (S2a’s analysis, unchanged) and the round-trip gate cannot be met without accept, so landing WSARecv/WSASend/CancelIoEx/WSAGetOverlappedResult alone would be exactly the half-displaced set the stage forbids. This lane stops at that boundary with the spec SETTLED rather than opening a displacement it could not also gate; the next lane implements §4.4 + §4.5 + the ruling above as written, against the §7 S2 gate pair (TcpLoopbackRoundTrip at VALUE level, plus the §5 deadline matrix).

5. The deadline/unblock story — the hard part, priced honestly

This section is the reason the board said “a deadline/unblock story to settle, not a wrapper repair.” The semantics to reproduce, each read out of runtime/netpoll.cs:

  1. d is a relative ns duration (setDeadlineImpl computes time.Until(t), fd_poll_runtime.cs:170): d > 0 arm; d == 0 NO deadline (clear); d < 0 already expired (setDeadlineImpl normalizes an exactly-now deadline to -1, :171–173). Go adds nanotime() and clamps overflow to int64.max (netpoll.cs:395–402); the managed mirror clamps the relative due-time to Timer’s ~49.7-day ceiling and re-arms on fire under a generation check — an honesty note, not a behavior change (Go’s ceiling is ~292 years; both are “never” for a socket deadline).
  2. mode is 'r', 'w', or 'r'+'w' — the combined form sets BOTH deadlines (:403–408). Go’s single-combo-timer optimization (:410–414) is NOT reproduced: two timers with the same due time are observationally equivalent, and the combo machinery (rtf selection, netpollDeadline firing both modes) exists to save a runtime timer, a resource the managed side is not short of.
  3. Expiry is STICKY per mode. On fire, rd/wd = -1 → the info bit → every subsequent prepare/wait in that mode returns pollErrTimeout (netpolldeadlineimpl :656–698, netpollcheckerr :544–546) until a LATER SetDeadline call rewrites that mode’s deadline — to zero (clears), to the future (re-arms), or to the past (re-expires). Managed: the expired flag, cleared/re-set on every pollSetDeadline for the modes it names. net’s deadline-dependent tests assert this sticky-until-reset shape; getting it wrong reads as “connection permanently broken after one timeout” or as “timeout not sticky” — both behaviorally loud.
  4. A deadline set in the past fires NOW, against the CURRENT waiter (:448–458): after updating state, wake the blocked mode(s) without setting ready. The waiter’s loop re-checks and returns pollErrTimeout; execIO then runs the cancellation path. Same wake-without-ready rule for pollUnblock (:473–505).
  5. Stale timer callbacks must be inert. Go guards with rseq/wseq — bumped on every deadline change and unblock — checked by the fired callback under the pd lock (:425, 481, 660–670). The managed mirror is FORCED into the same shape by .NET semantics: Timer.Change/Dispose do not synchronize with an in-flight callback, so the callback re-validates its captured generation under the desc lock and returns if it lost. This is not optional hardening; without it, a reset deadline can expire a fresh one.
  6. Expiry does not abandon the in-flight operation. After a timeout wake, execIO still owns a kernel-pending overlapped op: it issues CancelIoEx and then waitCanceled — which waits for completion-readiness ONLY, ignoring the very timeout that woke it (netpollblock(waitio: true), :377–382, 604). Liveness holds because the kernel ALWAYS posts a completion for a cancelled overlapped operation (with ERROR_OPERATION_ABORTED), and if the op won the race and completed first, CancelIoEx returns ERROR_NOT_FOUND and the completion is already in flight (execIO handles both, fd_windows.cs:212–231). Under mechanism (a) both arrive through the CLR callback → readywaitCanceled returns. The one configuration that would BREAK liveness is an operation whose completion packet was SKIPPED (skipSyncNotif) reaching the cancel path — it cannot: skip-on-success suppresses the packet only for a submit that returned synchronous success, and that path returns from execIO before any wait (fd_windows.cs:171–177); every path that CAN reach waitERROR_IO_PENDING, or sync success with the notification mode NOT skipped — has a completion packet guaranteed in flight. This invariant gets a dedicated test in the S2 matrix.
  7. Check order is fixed: closing > timeout (> eventErr, unix-only) — netpollcheckerr :539–554 — and readiness-consumption beats both on entry (:585–589): a completion that raced the deadline is still delivered to the caller, matching Go’s preference for returning real IO over a same-instant timeout.

Why not CancellationToken. The task’s framing asks this to be priced explicitly. A CancellationTokenSource models a one-shot cancellation of a linked operation; Go’s deadline is none of those things — it is (i) per-MODE, not per-operation; (ii) STICKY across future operations until explicitly re-set (a fired CTS cannot be un-cancelled; a fresh CTS per op loses the stickiness that lives BETWEEN ops); (iii) REPLACEABLE while an op is in flight (Timer.Changeseq++, without disturbing the op); and (iv) NON-ABANDONING — the timed-out op must still be cancelled-and-HARVESTED by the same caller (point 6), where CTS-style composition wants to throw/abandon at the wait site. Modeling all four with tokens reconstructs the flags+timer state machine anyway, plus an allocation per operation and a linked-registration lifetime problem. The recommendation (OQ4) is the direct map: System.Threading.Timer + sticky flags + generations under the desc lock — ~5 state fields and 2 timers per desc, with the complexity concentrated where it genuinely lives: the interleavings. The S2 gate matrix (§7) enumerates them adversarially rather than hoping.

The priced risk. The state machine is small; the race surface is not: completion vs timeout vs CancelIoEx vs Close, times skipSyncNotif, times deadline-replaced-mid-wait. The budget for this arc should assume the deadline matrix — not the happy-path round trip — is where the iteration goes. The mitigations built into the design: one lock (no lock-free interleavings to reason about), single-waiter-per-mode (no queue/fairness dimension), generation checks forced by .NET timer semantics, and the wake-vs-ready separation lifted intact from Go.

6. Blast radius — what stays converted, what becomes hand-owned

Stays converted, byte-for-byte (the point of the seam):

New hand-owned surface:

Artifact Kind Census effect
internal/poll/windows/runtime_netpoll_impl.cs — the ten bodies + ManagedPollDesc + delivery new _impl.cs, [module: go.GoManualConversion], no .go counterpart (never regenerated — the runtime_sema_impl.cs shape) +1 marked file
syscall/windows/zsyscall_windows_wsa_impl.cs (name per OQ6) — WSA family mirrors + op records new _impl.cs + manualConversionFuncs entries under "syscall" (goosWindows) +1 marked file; displaced wrappers regenerate as placeholders
internal/syscall/windows/windows/zsyscall_windows_wsa_impl.csWSAGetOverlappedResult (+S3: WSARecvMsg/WSASendMsg/WSASendtoInet4/6) same mechanism, new "internal/syscall/windows" key in manualConversionFuncs +1 marked file

Converter change: manualConversionFuncs map entries ONLY — data, not logic; no new converter .go file (no projitems entry owed); go test ./... and CNR classify the one-time regen of the displaced wrapper files as the change’s intended A/B footprint. The hand-own census GROWS — re-measure at the regen per the ritual, never carry forward (CLAUDE.md, corpus mechanics §1).

Adjacent walls this design deliberately does NOT claim (so nobody reads “netpoll landed” as “net validates”): net’s own runtime_rand bodyless stub (net/dnsclient.cs:20), the DNS resolver stack, GetIfEntry/FreeAddrInfoW (both still on the struct-passing census — net.Interfaces, DNS), and crypto/x509’s cert-store members. Each is a later, smaller arc with this design’s machinery as precedent.

7. Gates and staged landing

Stage discipline per the repo’s standing rules: behavioral guards compare REAL VALUES against go run (the LocalTimeZone/SockaddrRoundTrip doctrine — never absence-of-fault), outputs deterministic (ephemeral ports and timings printed as derived invariants, never raw). Each stage banks separately; a later stage blocked does not un-bank an earlier one.

S0 — mechanism in place, nothing reaches it. The three _impl.cs files + map entries + displaced-wrapper regen land compiling. Gates: full behavioral suite (existing 528 outputs unaffected — nothing exercises sockets today); check-no-regression clean EXCEPT the enumerated displaced-wrapper files (the intended footprint, named in the commit); converter go test ./...; go2cs.slnx + go2cs-stdlib.slnx build; filtered sweeps over the packages whose closure touches the displaced files — syscall 62/62 must hold (the banked row most exposed), plus os, path/filepath, time spot checks (their syscalls ride the untouched dispatcher, but the sweep is cheap and the claim should be measured, not argued).

S1 — net.Listen smoke. New behavioral test NetListenSmoke: Listen("tcp", "127.0.0.1:0"), assert/print invariants (addr network, port > 0, distinct second listener, Close, listen-after-close on the same port), byte-compared against go run. Exercises contracts 1, 2, 3, 8, 9 with zero data flow. This is the first observable retreat of the wall: the board’s internal/poll row stops dying in pollServerInit.

S2 — the round trip + the deadline matrix (the arc’s real gate). Two behavioral tests:

⚠ BLOCKER (S2b lane, measured 2026-08-14): TcpLoopbackRoundTrip is UNREACHABLE until the sockaddr DECODE is fixed, and it blocks the seam’s most dangerous surface from value-level verification. net’s accept path calls RawSockaddrAny.Sockaddr() on the GetAcceptExSockaddrs output (net/windows/fd_windows.cs:255–256), and that decode still carries the port ALIAS L10 hand-owned away on the ENCODE side — var p = (ж<array<byte>>)(uintptr)(new @unsafe.Pointer(pp.of(RawSockaddrInet4.ᏑPort))) (syscall/windows/syscall_windows.cs:953). An array<T> rebuilt from a raw address materializes length ZERO, so p[0] panics. Measured directly rather than inferred, with a throwaway probe that constructs an AF_INET RawSockaddrAny and calls Sockaddr(): Go answers decoded AF_INET port=0 addr=[0 0 0 0]; C# answers panic: runtime error: index out of range [0] with length 0.

L10 left this auto-converted deliberately and correctly: hand-owning it drops the three [assembly: GoImplement<Sockaddr{Inet4,Inet6,Unix}, ΔSockaddr>(Pointer = true)] records (syscall/windows/package_info.cs:45–47) that its body’s casts are the only witness for, and a MEASURED reconvert of net against the shortened package_info showed net minting duplicate adapters — the second-identity regression. The real answer L10 named is the converter’s POINTER method-set recording.

UNBLOCKED at the record level (2026-08-14, lane D). That converter increment has LANDED: recordSamePackageImplements now records the POINTER method set as well as the value one, behind the value form’s five gates plus a both-sides-EXPORTED gate (a (Pointer = true) record is consumed by NAMING the generated adapter, which ImplementGenerator scopes public only when both participants are). The three ΔSockaddr pairs are now sourced from types.Implements(*T, Sockaddr) instead of from (*RawSockaddrAny).Sockaddr’s casts, and the exact probe that measured the regression now proves its absence: with that method suppressed through manualConversionFuncs, syscall’s package_info.cs still carries all three records and a reconvert of net still references syscall.SockaddrInet4жΔSockaddr rather than minting its own. What this does and does not unblock: hand-owning RawSockaddrAny.Sockaddr is now safe from the second-identity consequence, so the port-alias decode defect can be fixed the same way L10 fixed its encode twin. It does NOT by itself fix that defect — the array<T>-from-raw-address seam is still there in the auto conversion — so TcpLoopbackRoundTrip remains blocked until someone takes the hand-own this increment made available.

What survives, precisely. A census of every .Sockaddr() call site in net finds six: the two accept-path sites above, three in interface_windows.cs (net.Interfaces) and one in dnsconfig_windows.cs — the last four already named as adjacent walls in §6. The DIAL path never touches it, because netFD’s local/peer addresses come from Getsockname/Getpeername, which L10 hand-owned to decode natively. So a dialed client conn is fully usable (the kernel completes the handshake from the listen backlog with no accept on the other end — the shape SockaddrRoundTrip already relies on), which makes NetDeadlineMatrix achievable in full and exercises five of the six wrappers; AcceptEx is reachable too, but only up to a DEADLINE-cancelled accept, never a successful one.

That is deliberately not treated as good enough to land the seam on. What no dial-only gate can prove is that bytes arrive correctly — the native WSABUF mirroring, the pinned user buffer and the transferred counts are exactly the “returns garbage without crashing” class the repo’s standing rule covers: verify at VALUE level, never at fault level. A submit seam whose buffer marshalling is unproven is the wrong thing to bank, which is why this lane stops at the boundary rather than landing ~530 lines against a gate it knows it cannot meet.

CLOSED 2026-08-14 (lane S2b, second attempt). The decode is hand-owned: RawSockaddrAny.Sockaddr joins manualConversionFuncs["syscall"] and syscall/windows/syscall_windows_impl.cs flattens the managed struct back to its 116-byte native image for the readNativeSockaddr the encoders already share. The record-survival witness this note demanded was re-measured on the lane’s own build, not inherited: with the body displaced, syscall’s package_info.cs still carries all three (Pointer = true) records and a seeded reconvert of net still references syscall.Sockaddr{Inet4,Inet6,Unix}жΔSockaddr at all seven sites with zero local adapters — the whole A/B footprint being ONE file, syscall_windows.cs. Guarded at value level by four new SockaddrRoundTrip lines (IPv4; an IPv6 address deliberately crossing the Addr.Data/Pad boundary; AF_UNIX; an unknown family answering EAFNOSUPPORT), byte-identical to go run.

TcpLoopbackRoundTrip remains blocked, on a DIFFERENT wall. The decode was the accept path’s first obstacle, not its last: §4.5’s GetAcceptExSockaddrs finding is the one that now holds it, and it wants a ruling before the displacement can be written. What this closure buys is that the blocker is no longer a converter capability question — it is a plumbing choice with two costed shapes.

Gates: full behavioral suite; then the pipeline’s own measure — filtered sweep of internal/poll: the board row’s target is 19/19 (from 18/19, sole miss runtime_pollServerInit — the row this design exists to close).

⚠ FINDING (implementation lane, 2026-08-13, measured at S1): that gate has an unrecorded PREREQUISITE — internal/poll’s converted test host does not currently BUILD, so the row is not measurable at any value, before or after this arc. Measuring it at S1 (regression diligence: S1 lets TestWSASocketConflict run past the fd.Init that used to kill it, so “further than it has ever run” is where a new hang would live) produced a C# compile error rather than a verdict:

export_test.cs(13,62): error CS0123: No overload for 'consume' matches
                       delegate 'Action<ж<slice<slice<byte>>>, long>'

export_test.go’s var Consume = consume is a func VALUE of a function whose *[][]byte parameter the production emission now lowers to a C# ref (consume(ref slice<slice<byte>> v, int64 n), fd.cs:92), while the test file still spells the delegate in the ж<T> box form. A ref-taking method cannot bind to that delegate. Both files are ordinary converted output that this arc does not touch — the netpoll hand-own supplies partial BODIES for the ten runtime_poll* methods and cannot alter consume’s signature — so this is the ref-lowering arc meeting func-value conversion, and it is not this arc’s to fix (recorded and handed to the coordinator rather than reached across, per OQ6’s ownership line). The board’s “18 of 19” reading therefore predates the ref-lowering landing and should be treated as stale until the host compiles again.

ATTRIBUTED AND CHARTERED ELSEWHERE (coordinator, 2026-08-14). This CS0123 is the first real corpus witness of the ж-box arc’s §3.5 func-value adapter gap — a Go func VALUE aliasing a ref-lowered function — which is precisely the evidence class A3 recorded as missing. It gets its own lane, and this note is where that lane starts. The netpoll arc does not fix it and does not wait for it: per the same ruling, S2 runs on its other gates and reports this row as blocked-with-cause, citing this error. When the lane lands, whoever re-measures should re-derive the target rather than expecting 19/19 — see the COM4 host-dependence below.

Two facts worth carrying to whoever picks it up. The Go side of the same run is itself not clean on every host: TestSerialFdsAreInitialised/COM4 fails wherever a real COM4 exists, so the differential’s Go baseline is host-dependent and the target may not be a round 19/19 — re-derive it at measure time rather than treating a non-round result as a miss.

The other fact this measurement was meant to probe — the FD.WSAIoctl foreign-overlapped hazard — is closed structurally at S2a and no longer needs probing: the CLR association moved from pollOpen to the first submit (§4.2’s amendment), so a socket that only ever sees foreign overlapped IO is never bound at all. That is why the unmeasurable row costs this arc nothing it cannot recover: the one thing the row was uniquely positioned to catch has been removed rather than merely watched for.

S3 — consumer re-measures (the board rows behind the netpoll wall). The RESOLVED note freezes the walled set and this design inherits it as its unlock ledger: net/smtp (9/14, five rows on this exact stack — the first re-measure, per the L10 spec’s consumer-proof pattern), then ONE of the L9-held socket rows (net/http/httptest recommended there), then the remainder as breadth lanes: net/http/cgi (36/39), net/http/httputil, net/http/cookiejar, net/rpc. UDP wrappers land here gated by whichever suite reaches them first. net itself stays a FUTURE arc — its suite needs the §6 adjacent walls (DNS, interfaces, runtime_rand) and its census should be taken fresh when this machinery exists, not promised now.

8. Non-goals

9. Open questions — RULED (coordinator, 2026-08-13)

All eight recommendations are RATIFIED as written. The arc is chartered; §4.2’s ThreadPoolBoundHandle plumbing, §4.3.3’s displacement mechanism and staging, §5’s deadline semantics list, and §6’s file placement are now the implementation contract. One clause stays live by design: OQ3’s UDP-fold — if the S2 sweep shows UDP-shaped internal/poll tests failing on the stubs, UDP folds into S2 without re-ruling. Each item below retains its original recommendation text as the record of what was ratified and why.