#include "network_internal.h" #include "guest_abi/guest_region.h" #include "fiber_manager.h" #include "hle/net/network.h" #include "ppc_runtime.h" #include "runtime_log.h" void OS_HLE_WakeupThreadNoReschedule(CpuContext* ctx, uint32_t waitQueue); void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg); namespace NetworkHle { enum class DeferredDnsKind { GetHostByName, GetAddrInfo, InetAton, }; static const char* DeferredDnsKindName(DeferredDnsKind kind) { switch (kind) { case DeferredDnsKind::GetHostByName: return "gethostbyname"; case DeferredDnsKind::GetAddrInfo: return "getaddrinfo"; case DeferredDnsKind::InetAton: return "inet_aton"; } return "?"; } enum class DeferredNetworkCompletionKind { SyncWaitQueue, AsyncCallback, }; struct DeferredNetworkRoute { DeferredNetworkCompletionKind kind = DeferredNetworkCompletionKind::SyncWaitQueue; uint64_t token = 0; uint32_t waitQueue = 0; uint32_t expectedThread = 0; uint32_t callback = 0; uint32_t callbackArg = 0; }; // This is deliberately a host-only value object. Guest strings, vectors and // hints are copied by the emulation thread before the worker is launched. The // worker may carry guest output addresses as opaque integers, but it has no // CpuContext or guest-memory pointer with which to dereference them. struct DeferredDnsWork { DeferredDnsKind kind = DeferredDnsKind::GetHostByName; DeferredNetworkRoute route{}; std::string node; std::string service; bool hasHints = false; int hintFlags = 0; int hintFamily = AF_UNSPEC; int hintSockType = 0; int hintProtocol = 0; uint32_t outAddress = 0; uint32_t outSize = 0; }; struct DeferredDnsAddress { int flags = 0; uint32_t family = 0; int sockType = 0; int protocol = 0; uint32_t addressLength = 0; uint32_t ipv4Address = 0; std::array socketData{}; }; static std::optional NormalizeDeferredDnsAddress(const addrinfo& native) { // This IOS result layout currently owns one complete IPv4 sockaddr. Do not // advertise an IPv6 ai_addrlen while retaining only sockaddr::sa_data. if (!native.ai_addr || !NetworkDeferredContract::CanCopyIpv4SockAddr( native.ai_family, native.ai_addrlen, AF_INET)) { return std::nullopt; } const int wiiFamily = MapNativeAfToWii(native.ai_family); if (wiiFamily != kWiiAfInet) { return std::nullopt; } DeferredDnsAddress normalized{}; normalized.flags = native.ai_flags; normalized.family = static_cast(wiiFamily); normalized.sockType = native.ai_socktype; normalized.protocol = native.ai_protocol; normalized.addressLength = static_cast(NetworkDeferredContract::kWiiIpv4SockAddrBytes); const auto* socketAddress = reinterpret_cast(native.ai_addr); std::memcpy(normalized.socketData.data(), socketAddress->sa_data, normalized.socketData.size()); const auto* ipv4 = reinterpret_cast(native.ai_addr); normalized.ipv4Address = ntohl(ipv4->sin_addr.s_addr); return normalized; } struct DeferredDnsCompletion { DeferredDnsWork work{}; int32_t result = 0; std::vector addresses; }; struct DeferredPollWork { DeferredNetworkRoute route{}; NetworkPollContract::Timeout timeout{}; uint32_t outAddress = 0; uint32_t outSize = 0; std::vector descriptors; }; struct DeferredConnectWork { DeferredNetworkRoute route{}; NetworkPollContract::Timeout timeout{}; uint32_t wiiFd = 0; NativeSocket nativeFd = kInvalidSocket; uint64_t socketGeneration = 0; sockaddr_in peerAddress{}; std::optional initialResult; }; using DeferredDnsPreparation = NetworkDeferredContract::Preparation; using DeferredPollPreparation = NetworkDeferredContract::Preparation; using DeferredConnectPreparation = NetworkDeferredContract::Preparation; struct DeferredNetworkStore { std::mutex completedMutex; std::deque completed; // Pending polls are submitted and probed only by the emulation scheduler // thread, so unlike DNS worker completions they need no cross-thread lock. std::vector pendingPolls; std::vector pendingConnects; std::mutex resultMutex; std::unordered_map syncResults; std::mutex schedulerThreadMutex; std::optional schedulerThread; std::atomic nextToken{1}; }; static DeferredNetworkStore& GetDeferredNetworkStore() { // DNS workers are detached so the resolver can never block runtime // shutdown. Keep their destination alive for the lifetime of the process; // there is intentionally no static destructor for this state. static auto* store = new DeferredNetworkStore(); return *store; } // pendingConnects is only ever touched from the emulation/scheduler thread, so // this needs no lock. Marking the operation resolved makes the next completion // pump wake the parked OSThread with the requested result; the connect itself is // never re-probed afterwards. void AbortPendingDeferredConnects(uint32_t wiiFd, int32_t result) { DeferredNetworkStore& store = GetDeferredNetworkStore(); for (DeferredConnectWork& work : store.pendingConnects) { if (work.wiiFd == wiiFd && !work.initialResult) { work.initialResult = result; } } } static void NoteDeferredNetworkSchedulerThread(const char* operation) { DeferredNetworkStore& store = GetDeferredNetworkStore(); const std::thread::id current = std::this_thread::get_id(); std::lock_guard lock(store.schedulerThreadMutex); if (!store.schedulerThread) { store.schedulerThread = current; return; } if (*store.schedulerThread != current) { RT_LOGF(RT_TAG_NET, "deferred network %s ran off the emulation thread\n", operation); #ifndef NDEBUG assert(*store.schedulerThread == current); #endif } } static int32_t DeferredDnsFailureResult(DeferredDnsKind kind) { switch (kind) { case DeferredDnsKind::GetHostByName: return -1; case DeferredDnsKind::GetAddrInfo: return SO_ERROR_HOST_NOT_FOUND; case DeferredDnsKind::InetAton: return 0; } return -1; } static void QueueDeferredDnsCompletion(DeferredDnsCompletion completion) { DeferredNetworkStore& store = GetDeferredNetworkStore(); std::lock_guard lock(store.completedMutex); store.completed.push_back(std::move(completion)); } static DeferredDnsCompletion ResolveDeferredDns(DeferredDnsWork work) { DeferredDnsCompletion completion{}; completion.work = std::move(work); addrinfo hints{}; addrinfo* hintPtr = nullptr; if (completion.work.kind != DeferredDnsKind::GetAddrInfo) { hints.ai_family = AF_INET; hintPtr = &hints; } else { if (completion.work.hasHints) { #ifdef AF_INET6 if (completion.work.hintFamily == AF_INET6) { // The guest result encoder deliberately owns IPv4 sockaddr // payloads only. An IPv6-only query therefore has no valid // result rather than a truncated sockaddr_in6. completion.result = DeferredDnsFailureResult(completion.work.kind); return completion; } #endif hints.ai_flags = completion.work.hintFlags; hints.ai_family = completion.work.hintFamily == AF_UNSPEC ? AF_INET : completion.work.hintFamily; hints.ai_socktype = completion.work.hintSockType; hints.ai_protocol = completion.work.hintProtocol; } else { hints.ai_family = AF_INET; } hintPtr = &hints; } addrinfo* nativeResults = nullptr; const char* node = completion.work.node.empty() ? nullptr : completion.work.node.c_str(); const char* service = completion.work.service.empty() ? nullptr : completion.work.service.c_str(); const int gai = getaddrinfo(node, service, hintPtr, &nativeResults); if (gai != 0 || !nativeResults) { completion.result = DeferredDnsFailureResult(completion.work.kind); if (nativeResults) { freeaddrinfo(nativeResults); } // The name the guest asked for is what proves whether the Retro-WFC // payload's own resolver hooks rewrote the Nintendo hostname or not. NetFail("dns %s '%s'%s%s FAILED gai=%d result=%d", DeferredDnsKindName(completion.work.kind), completion.work.node.c_str(), completion.work.service.empty() ? "" : " service=", completion.work.service.c_str(), gai, completion.result); return completion; } // The host resolver's linked list and sockaddr pointers are owned by its // allocator. Normalize everything to POD before leaving the worker. const size_t maxAddresses = completion.work.kind == DeferredDnsKind::GetHostByName ? 71u : (completion.work.kind == DeferredDnsKind::InetAton ? 1u : 256u); completion.addresses.reserve(std::min(maxAddresses, 16u)); for (addrinfo* it = nativeResults; it && completion.addresses.size() < maxAddresses; it = it->ai_next) { if (auto normalized = NormalizeDeferredDnsAddress(*it)) { completion.addresses.push_back(std::move(*normalized)); } } freeaddrinfo(nativeResults); completion.result = completion.addresses.empty() ? DeferredDnsFailureResult(completion.work.kind) : 0; return completion; } static bool LaunchDeferredDns(DeferredDnsWork work) { std::unique_ptr worker; try { worker = std::make_unique([work = std::move(work)]() mutable { NetworkDeferredContract::RunWorker( std::move(work), [](const DeferredDnsWork& copiedWork) { return ResolveDeferredDns(copiedWork); }, [](DeferredDnsCompletion completion) { QueueDeferredDnsCompletion(std::move(completion)); }, [](DeferredDnsWork failedWork, std::exception_ptr error) { (void)error; DeferredDnsCompletion failure{}; failure.work = std::move(failedWork); failure.result = DeferredDnsFailureResult(failure.work.kind); try { QueueDeferredDnsCompletion(std::move(failure)); } catch (const std::exception& queueError) { RT_LOGF(RT_TAG_NET, "failed to queue deferred DNS failure: %s\n", queueError.what()); } catch (...) { RT_LOGF(RT_TAG_NET, "failed to queue deferred DNS failure\n"); } }); }); } catch (const std::exception&) { return false; } (void)NetworkDeferredContract::DetachOrRelease( std::move(worker), [](std::exception_ptr error) noexcept { try { if (error) { std::rethrow_exception(error); } } catch (const std::exception& detachError) { RT_LOGF(RT_TAG_NET, "failed to detach deferred DNS worker; " "retaining running worker: %s\n", detachError.what()); return; } catch (...) { } RT_LOGF(RT_TAG_NET, "failed to detach deferred DNS worker; " "retaining running worker\n"); }); return true; } // Every deferred operation is an /dev/net/ip/top command; anything else must // fall straight through to the direct path. static bool IsIpTopDevice(uint32_t fd) { const std::optional kind = GetDeviceKind(fd); return kind && *kind == DeviceKind::IpTop; } static bool IsIpTopCommand(uint32_t fd, uint32_t cmd, uint32_t expectedCmd) { return cmd == expectedCmd && IsIpTopDevice(fd); } template static bool IsApplicable(const NetworkDeferredContract::Preparation& prepared) { return prepared.State() != NetworkDeferredContract::PreparationState::NotApplicable; } static DeferredDnsPreparation PrepareDeferredGetHostByName( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen) { if (!IsIpTopCommand(fd, cmd, IOCTL_SO_GETHOSTBYNAME)) { return DeferredDnsPreparation::NotApplicable(); } if (!inBuf || !outBuf || outLen < 0x460 || !Memory::Contains(outBuf, outLen)) { return DeferredDnsPreparation::Error(-1); } const uint32_t maxInput = inLen ? inLen : 256u; if (!Memory::Contains(inBuf, maxInput)) { return DeferredDnsPreparation::Error(-1); } DeferredDnsWork work{}; work.kind = DeferredDnsKind::GetHostByName; work.node = ReadGuestString(inBuf, maxInput); if (work.node.empty()) { return DeferredDnsPreparation::Error(-1); } work.outAddress = outBuf; work.outSize = outLen; return DeferredDnsPreparation::Ready(std::move(work), -1); } static DeferredDnsPreparation PrepareDeferredInetAton( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen) { if (!IsIpTopCommand(fd, cmd, IOCTL_SO_INETATON)) { return DeferredDnsPreparation::NotApplicable(); } const uint32_t maxInput = inLen ? inLen : 256u; if (!inBuf || !outBuf || outLen < 4u || !Memory::Contains(inBuf, maxInput) || !Memory::Contains(outBuf, 4u)) { return DeferredDnsPreparation::Error(0); } DeferredDnsWork work{}; work.kind = DeferredDnsKind::InetAton; work.node = ReadGuestString(inBuf, maxInput); if (work.node.empty()) { return DeferredDnsPreparation::Error(0); } in_addr numericAddress{}; if (inet_pton(AF_INET, work.node.c_str(), &numericAddress) == 1) { // Numeric conversion cannot block; retain the existing immediate path. return DeferredDnsPreparation::NotApplicable(); } work.outAddress = outBuf; work.outSize = 4u; return DeferredDnsPreparation::Ready(std::move(work), 0); } static DeferredDnsPreparation PrepareDeferredScalarDns( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen) { auto getHostByName = PrepareDeferredGetHostByName(fd, cmd, inBuf, inLen, outBuf, outLen); if (IsApplicable(getHostByName)) { return getHostByName; } return PrepareDeferredInetAton(fd, cmd, inBuf, inLen, outBuf, outLen); } static DeferredPollPreparation PrepareDeferredPoll( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen) { if (!IsIpTopCommand(fd, cmd, IOCTL_SO_POLL)) { return DeferredPollPreparation::NotApplicable(); } uint32_t descriptorCount = 0; if (!ValidatePollRequest(inBuf, inLen, outBuf, outLen, descriptorCount)) { return DeferredPollPreparation::Error(-SO_EINVAL); } const int64_t timeoutMilliseconds = static_cast(Memory::Read64(inBuf)); if (!NetworkPollContract::RequiresSchedulerWait(timeoutMilliseconds)) { // A timeout-zero poll is guaranteed not to block and is cheaper to // execute directly than to copy, sleep, wake, and copy back. return DeferredPollPreparation::NotApplicable(); } DeferredPollWork work{}; work.outAddress = outBuf; work.outSize = outLen; work.timeout = NetworkPollContract::Timeout::FromMilliseconds(timeoutMilliseconds); work.descriptors = CopyPollDescriptors(outBuf, descriptorCount); return DeferredPollPreparation::Ready(std::move(work), -SO_EINVAL); } static DeferredConnectPreparation PrepareDeferredConnect( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen) { if (!IsIpTopCommand(fd, cmd, IOCTL_SO_CONNECT)) { return DeferredConnectPreparation::NotApplicable(); } if (!inBuf || inLen < 16u || !Memory::Contains(inBuf, 16u)) { return DeferredConnectPreparation::Error(-SO_EINVAL); } const uint32_t wiiFd = Memory::Read32(inBuf); WiiSocket* socket = GetWiiSocket(wiiFd); if (!socket) { return DeferredConnectPreparation::Error(-SO_EBADF); } if (socket->nonblocking) { // The direct path is already a zero-wait host connect and returns // SO_EINPROGRESS to the guest as required. return DeferredConnectPreparation::NotApplicable(); } DeferredConnectWork work{}; work.wiiFd = wiiFd; work.nativeFd = socket->native; work.socketGeneration = socket->generation; work.peerAddress = ReadWiiSockAddr(inBuf + 8u); work.timeout = NetworkPollContract::Timeout::FromMilliseconds( NetworkConnectContract::kGuestBlockingTimeoutMilliseconds); return DeferredConnectPreparation::Ready(std::move(work), -SO_EINVAL); } static DeferredDnsPreparation PrepareDeferredGetAddrInfo( uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) { if (!IsIpTopCommand(fd, cmd, IOCTLV_SO_GETADDRINFO)) { return DeferredDnsPreparation::NotApplicable(); } const uint64_t vectorCount = static_cast(numIn) + numOut; if (vectorCount == 0 || vectorCount > UINT32_MAX || !vectorPtr || !Memory::Contains(vectorPtr, static_cast(vectorCount) * 8u)) { return DeferredDnsPreparation::Error(-SO_EINVAL); } const std::vector vectors = ReadVectors(vectorPtr, static_cast(vectorCount)); const std::vector in(vectors.begin(), vectors.begin() + numIn); const std::vector out(vectors.begin() + numIn, vectors.end()); if (out.empty() || !out[0].address || !out[0].size || !Memory::Contains(out[0].address, out[0].size)) { return DeferredDnsPreparation::Error(-SO_EINVAL); } for (const IoVector& vector : vectors) { if ((vector.address == 0) != (vector.size == 0) || (vector.address && !Memory::Contains(vector.address, vector.size))) { return DeferredDnsPreparation::Error(-SO_EINVAL); } } DeferredDnsWork work{}; work.kind = DeferredDnsKind::GetAddrInfo; work.outAddress = out[0].address; work.outSize = out[0].size; if (!in.empty() && in[0].address && in[0].size) { if (!Memory::Contains(in[0].address, in[0].size)) { return DeferredDnsPreparation::Error(-SO_EINVAL); } work.node = ReadGuestString(in[0].address, in[0].size); } if (in.size() > 1 && in[1].address && in[1].size) { if (!Memory::Contains(in[1].address, in[1].size)) { return DeferredDnsPreparation::Error(-SO_EINVAL); } work.service = ReadGuestString(in[1].address, in[1].size); } if (in.size() > 2 && in[2].address) { if (in[2].size < 0x10 || !Memory::Contains(in[2].address, 0x10)) { return DeferredDnsPreparation::Error(-SO_EINVAL); } work.hasHints = true; work.hintFlags = static_cast(Memory::Read32(in[2].address)); work.hintFamily = MapWiiAddrInfoFamily(Memory::Read32(in[2].address + 4)); work.hintSockType = MapWiiAddrInfoSocketType(Memory::Read32(in[2].address + 8)); work.hintProtocol = static_cast(Memory::Read32(in[2].address + 0x0C)); } if (work.node.empty() && work.service.empty()) { return DeferredDnsPreparation::Error(SO_ERROR_HOST_NOT_FOUND); } return DeferredDnsPreparation::Ready( std::move(work), SO_ERROR_HOST_NOT_FOUND); } static int32_t ApplyDeferredDnsCompletion(const DeferredDnsCompletion& completion) { if (completion.result != 0) { return completion.result; } const uint32_t outAddress = completion.work.outAddress; const uint32_t outSize = completion.work.outSize; if (!outAddress || !outSize || !Memory::Contains(outAddress, outSize)) { return -SO_EINVAL; } if (completion.work.kind == DeferredDnsKind::InetAton) { if (completion.addresses.empty()) { return 0; } Memory::Write32(outAddress, completion.addresses.front().ipv4Address); return 1; } ZeroMemoryRange(outAddress, outSize); if (completion.work.kind == DeferredDnsKind::GetHostByName) { constexpr uint32_t kStructSize = 0x10; constexpr uint32_t kIpListOffset = 0x110; constexpr uint32_t kIpPtrListOffset = 0x340; const std::string canonical = completion.work.node.size() < 240 ? completion.work.node : completion.work.node.substr(0, 239); for (uint32_t i = 0; i <= canonical.size(); ++i) { Memory::Write8(outAddress + kStructSize + i, i < canonical.size() ? static_cast(canonical[i]) : 0); } Memory::Write32(outAddress, outAddress + kStructSize); Memory::Write16(outAddress + 8, kWiiAfInet); Memory::Write16(outAddress + 10, 4); Memory::Write32(outAddress + 12, outAddress + kIpPtrListOffset); const uint32_t count = static_cast(std::min(completion.addresses.size(), 71u)); for (uint32_t i = 0; i < count; ++i) { Memory::Write32(outAddress + kIpListOffset + i * 4, completion.addresses[i].ipv4Address); Memory::Write32(outAddress + kIpPtrListOffset + i * 4, outAddress + kIpListOffset + i * 4); } Memory::Write32(outAddress + kIpPtrListOffset + count * 4, 0); // Hardware returns an empty aliases list. Point h_aliases at the same // terminator that ends h_addr_list, matching IOS/Dolphin exactly. Memory::Write32(outAddress + 4, outAddress + kIpPtrListOffset + count * 4); return 0; } constexpr uint32_t kInfoSize = 0x20; constexpr uint32_t kSockAddrOffset = 0x460; constexpr uint32_t kSockAddrStride = 0x1C; const uint64_t end = static_cast(outAddress) + outSize; const uint64_t infoEnd = std::min(end, static_cast(outAddress) + kSockAddrOffset); uint32_t infoAddress = outAddress; uint32_t socketAddress = outAddress + kSockAddrOffset; size_t writeCount = 0; while (writeCount < completion.addresses.size() && static_cast(infoAddress) + kInfoSize <= infoEnd && static_cast(socketAddress) + kSockAddrStride <= end) { ++writeCount; infoAddress += kInfoSize; socketAddress += kSockAddrStride; } infoAddress = outAddress; socketAddress = outAddress + kSockAddrOffset; for (size_t i = 0; i < writeCount; ++i) { const DeferredDnsAddress& address = completion.addresses[i]; if (address.family != kWiiAfInet || address.addressLength != NetworkDeferredContract::kWiiIpv4SockAddrBytes || !NetworkDeferredContract::AdvertisedSockAddrFits(address.addressLength)) { return -SO_EINVAL; } Memory::Write32(infoAddress, static_cast(address.flags)); Memory::Write32(infoAddress + 0x04, address.family); Memory::Write32(infoAddress + 0x08, MapNativeSocketTypeToWii(address.sockType)); Memory::Write32(infoAddress + 0x0C, static_cast(address.protocol)); Memory::Write32(infoAddress + 0x10, address.addressLength); Memory::Write32(infoAddress + 0x14, 0); Memory::Write32(infoAddress + 0x18, socketAddress); Memory::Write8(socketAddress, static_cast(address.addressLength & 0xFFu)); Memory::Write8(socketAddress + 1, static_cast(address.family & 0xFFu)); CopyToGuest(socketAddress + 2, address.socketData.data(), static_cast(address.socketData.size())); Memory::Write32(infoAddress + 0x1C, i + 1 < writeCount ? infoAddress + kInfoSize : 0); infoAddress += kInfoSize; socketAddress += kSockAddrStride; } return 0; } } // namespace NetworkHle using namespace NetworkHle; static bool InitializeDeferredSyncRoute(DeferredNetworkRoute& route, uint32_t waitQueue) { if (!waitQueue) { return false; } NoteDeferredNetworkSchedulerThread("submission"); DeferredNetworkStore& store = GetDeferredNetworkStore(); uint64_t nextToken = store.nextToken.fetch_add(1, std::memory_order_relaxed); if (nextToken == 0) { nextToken = store.nextToken.fetch_add(1, std::memory_order_relaxed); } // OSSleepThread uses the running OSThread, or the SDK default thread during // early scheduler setup. Capture its identity before yielding so a late // resolver result cannot wake a stale/reused stack queue after cancellation. constexpr uint32_t kOSRunningContextAddr = 0x800000E4u; const uint32_t kDefaultThreadContextAddr = MKW_GADDR(80347498); uint32_t expectedThread = Memory::Read32(kOSRunningContextAddr); if (expectedThread == 0) { expectedThread = kDefaultThreadContextAddr; } if (!Memory::Contains(expectedThread + 0x2C8u, 0x20u) || !Memory::Contains(waitQueue, 16u)) { return false; } route.kind = DeferredNetworkCompletionKind::SyncWaitQueue; route.token = nextToken; route.waitQueue = waitQueue; route.expectedThread = expectedThread; // ABA cookie: thread/stack addresses are reused after cancellation. Memory::Write64(waitQueue + 8u, nextToken); return true; } static void InitializeDeferredAsyncRoute(DeferredNetworkRoute& route, uint32_t callback, uint32_t callbackArg) { NoteDeferredNetworkSchedulerThread("submission"); route.kind = DeferredNetworkCompletionKind::AsyncCallback; route.callback = callback; route.callbackArg = callbackArg; } using DeferredStartOutcome = NetworkDeferredContract::StartOutcome; // Install a prepared operation on either completion route. The sync/async delta // is entirely in `initializeRoute`: the sync form allocates the token that // Network_HLE_TakeSyncResult later redeems, the async form leaves route.token at // zero, which StartPrepared documents as valid for a callback route. template static DeferredStartOutcome StartDeferred(NetworkDeferredContract::Preparation prepared, InitializeRoute&& initializeRoute, Install&& install) { return NetworkDeferredContract::StartPrepared( std::move(prepared), [&initializeRoute, &install](Work work) -> std::optional { if (!EnsureSocketRuntime() || !initializeRoute(work.route)) { return std::nullopt; } const uint64_t token = work.route.token; if (!install(std::move(work))) { return std::nullopt; } return token; }); } static bool DeferredConnectSocketIsStillValid(const DeferredConnectWork& work) { return SocketIdentityIsCurrent(work.wiiFd, work.nativeFd, work.socketGeneration); } static bool InstallDeferredDns(DeferredDnsWork work) { return LaunchDeferredDns(std::move(work)); } static bool InstallDeferredPoll(DeferredPollWork work) { GetDeferredNetworkStore().pendingPolls.push_back(std::move(work)); return true; } // A blocking connect issues the host connect() at submission time, because the // SDK's own retry loop depends on seeing the immediate outcome; only a genuinely // in-flight connect is parked for the completion pump to probe. static bool InstallDeferredConnect(DeferredConnectWork work) { if (!DeferredConnectSocketIsStillValid(work)) { work.initialResult = -SO_EBADF; } else { const int ret = connect(work.nativeFd, reinterpret_cast(&work.peerAddress), sizeof(work.peerAddress)); if (ret == 0) { work.initialResult = 0; } else { const int nativeError = NormalizeConnectError(NativeLastError()); if (!IsWouldBlockError(nativeError)) { const int32_t translated = TranslateSocketError(nativeError, false); // Dolphin rewrites -SO_EISCONN to SO_SUCCESS here (IOS/Network/ // Socket.cpp:356-359): the SDK detects a completed blocking // connect by re-issuing connect() on it, so reporting EISCONN // as an error aborted session setup. work.initialResult = translated == -SO_EISCONN ? 0 : translated; } } } GetDeferredNetworkStore().pendingConnects.push_back(std::move(work)); return true; } static auto SyncRoute(uint32_t waitQueue) { return [waitQueue](DeferredNetworkRoute& route) { return InitializeDeferredSyncRoute(route, waitQueue); }; } static auto AsyncRoute(uint32_t callback, uint32_t callbackArg) { return [callback, callbackArg](DeferredNetworkRoute& route) { InitializeDeferredAsyncRoute(route, callback, callbackArg); return true; }; } static std::optional ScalarDeferredFailureResult(uint32_t fd, uint32_t cmd) { if (!IsIpTopDevice(fd)) { return std::nullopt; } switch (cmd) { case IOCTL_SO_CONNECT: return -SO_EINVAL; case IOCTL_SO_POLL: return -SO_EINVAL; case IOCTL_SO_GETHOSTBYNAME: return -1; case IOCTL_SO_INETATON: return 0; default: return std::nullopt; } } // Guest-input marshalling faults must never escape into the IOS bridge. Note // that the two failure results are NOT the same for the ioctlv entry points: a // faulted descriptor array is a malformed request (-SO_EINVAL) while any other // failure is reported to the resolver's caller as a lookup miss. template static DeferredStartOutcome RunDeferredEntry(int32_t accessViolationResult, int32_t exceptionResult, Body&& body) { try { return body(); } catch (const Memory::AccessViolation&) { return DeferredStartOutcome::Immediate(accessViolationResult); } catch (const std::bad_alloc&) { return DeferredStartOutcome::Immediate(-SO_ENOMEM); } catch (const std::exception&) { return DeferredStartOutcome::Immediate(exceptionResult); } } // SO_CONNECT, SO_POLL and the scalar DNS commands all arrive through ioctl and // are distinguished by which Prepare* claims them. template static DeferredStartOutcome StartScalarDeferredIoctl( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen, InitializeRoute initializeRoute) { auto connect = PrepareDeferredConnect(fd, cmd, inBuf, inLen); if (IsApplicable(connect)) { return StartDeferred(std::move(connect), initializeRoute, InstallDeferredConnect); } auto poll = PrepareDeferredPoll(fd, cmd, inBuf, inLen, outBuf, outLen); if (IsApplicable(poll)) { return StartDeferred(std::move(poll), initializeRoute, InstallDeferredPoll); } return StartDeferred(PrepareDeferredScalarDns(fd, cmd, inBuf, inLen, outBuf, outLen), initializeRoute, InstallDeferredDns); } NetworkDeferredContract::StartOutcome Network_HLE_StartIoctlSync( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen, uint32_t waitQueue) { const std::optional failureResult = ScalarDeferredFailureResult(fd, cmd); if (!failureResult) { return DeferredStartOutcome::NotApplicable(); } return RunDeferredEntry(*failureResult, *failureResult, [&] { return StartScalarDeferredIoctl(fd, cmd, inBuf, inLen, outBuf, outLen, SyncRoute(waitQueue)); }); } NetworkDeferredContract::StartOutcome Network_HLE_StartIoctlvSync( uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr, uint32_t waitQueue) { if (!IsIpTopCommand(fd, cmd, IOCTLV_SO_GETADDRINFO)) { return DeferredStartOutcome::NotApplicable(); } return RunDeferredEntry(-SO_EINVAL, SO_ERROR_HOST_NOT_FOUND, [&] { return StartDeferred(PrepareDeferredGetAddrInfo(fd, cmd, numIn, numOut, vectorPtr), SyncRoute(waitQueue), InstallDeferredDns); }); } NetworkDeferredContract::StartOutcome Network_HLE_StartIoctlAsync( uint32_t fd, uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf, uint32_t outLen, uint32_t callback, uint32_t callbackArg) { const std::optional failureResult = ScalarDeferredFailureResult(fd, cmd); if (!failureResult) { return DeferredStartOutcome::NotApplicable(); } return RunDeferredEntry(*failureResult, *failureResult, [&] { return StartScalarDeferredIoctl(fd, cmd, inBuf, inLen, outBuf, outLen, AsyncRoute(callback, callbackArg)); }); } NetworkDeferredContract::StartOutcome Network_HLE_StartIoctlvAsync( uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr, uint32_t callback, uint32_t callbackArg) { if (!IsIpTopCommand(fd, cmd, IOCTLV_SO_GETADDRINFO)) { return DeferredStartOutcome::NotApplicable(); } return RunDeferredEntry(-SO_EINVAL, SO_ERROR_HOST_NOT_FOUND, [&] { return StartDeferred(PrepareDeferredGetAddrInfo(fd, cmd, numIn, numOut, vectorPtr), AsyncRoute(callback, callbackArg), InstallDeferredDns); }); } bool Network_HLE_TakeSyncResult(uint64_t token, int32_t* result) { if (!token || !result) { return false; } NoteDeferredNetworkSchedulerThread("result retrieval"); DeferredNetworkStore& store = GetDeferredNetworkStore(); std::lock_guard lock(store.resultMutex); const auto it = store.syncResults.find(token); if (it == store.syncResults.end()) { return false; } *result = it->second; store.syncResults.erase(it); return true; } static bool DeferredNetworkWaiterIsValid(const DeferredNetworkRoute& route) { if (!route.waitQueue || !route.expectedThread || !Memory::Contains(route.waitQueue, 16u) || !Memory::Contains(route.expectedThread + 0x2C8u, 0x20u) || Fiber::GuestFiberManager::IsTerminated(route.expectedThread)) { return false; } constexpr uint16_t kThreadStateWaiting = 4; constexpr uint32_t kThreadStateOffset = 0x2C8u; constexpr uint32_t kThreadQueueOffset = 0x2DCu; constexpr uint32_t kThreadNextOffset = 0x2E0u; constexpr uint32_t kThreadPrevOffset = 0x2E4u; try { return Memory::Read16(route.expectedThread + kThreadStateOffset) == kThreadStateWaiting && Memory::Read32(route.expectedThread + kThreadQueueOffset) == route.waitQueue && Memory::Read32(route.waitQueue) == route.expectedThread && Memory::Read32(route.waitQueue + 4u) == route.expectedThread && Memory::Read64(route.waitQueue + 8u) == route.token && Memory::Read32(route.expectedThread + kThreadNextOffset) == 0 && Memory::Read32(route.expectedThread + kThreadPrevOffset) == 0; } catch (const Memory::AccessViolation&) { return false; } } static void DeliverDeferredNetworkResult(CpuContext* cpu, DeferredNetworkStore& store, const DeferredNetworkRoute& route, int32_t result) { if (route.kind == DeferredNetworkCompletionKind::SyncWaitQueue) { { std::lock_guard lock(store.resultMutex); const auto [it, inserted] = store.syncResults.emplace(route.token, result); if (!inserted) { RT_LOGF(RT_TAG_NET, "duplicate deferred network token %" PRIu64 "\n", route.token); it->second = result; } } // Output and result are committed before the waiter becomes READY. OS_HLE_WakeupThreadNoReschedule(cpu, route.waitQueue); } else { // NandProcessPendingCallbacks runs immediately after this pump, on // this same emulation/scheduler host thread. NandQueueIosCallback(route.callback, result, route.callbackArg); } } static std::optional ProbeDeferredPoll( DeferredPollWork& work, NetworkPollContract::Timeout::TimePoint now) { const int nativeResult = NetworkPollContract::ProbeNow( work.descriptors, NetworkHle::CopiedPollSocketIsStillValid); const int32_t result = nativeResult < 0 ? SocketResult(nativeResult, false) : nativeResult; if (work.timeout.ShouldRemainPending(nativeResult, now)) { return std::nullopt; } return result; } static int32_t ApplyDeferredPollCompletion(const DeferredPollWork& work, int32_t result) { const size_t outputBytes = work.descriptors.size() * 12u; if (!work.outAddress || outputBytes == 0 || outputBytes > work.outSize || !Memory::Contains(work.outAddress, outputBytes)) { return -SO_EINVAL; } WritePollResults(work.outAddress, work.descriptors); return result; } static std::optional ProbeDeferredConnect( DeferredConnectWork& work, NetworkPollContract::Timeout::TimePoint now) { if (work.initialResult) { return *work.initialResult; } const bool identityIsCurrent = DeferredConnectSocketIsStillValid(work); int pollResult = 0; if (identityIsCurrent) { pollResult = ProbeConnectSettled(work.nativeFd, 0); } using NetworkConnectContract::ProbeDisposition; switch (NetworkConnectContract::ClassifyProbe( identityIsCurrent, pollResult, work.timeout.IsExpired(now))) { case ProbeDisposition::StaleSocket: return -SO_EBADF; case ProbeDisposition::PollError: return TranslateSocketError(NativeLastError(), false); case ProbeDisposition::TimedOut: // Dolphin's blocking-connect timeout reports -SO_ENETUNREACH // (IOS/Network/Socket.cpp:352-356). -SO_ETIMEDOUT is not reachable // through its error table at all, so the SDK has no handler for it. return -SO_ENETUNREACH; case ProbeDisposition::Pending: return std::nullopt; case ProbeDisposition::SocketReady: break; } return ClassifySettledConnect(work.nativeFd); } bool Network_HLE_ProcessCompletions(CpuContext* cpu) { if (!cpu) { return false; } NoteDeferredNetworkSchedulerThread("completion pump"); std::deque ready; DeferredNetworkStore& store = GetDeferredNetworkStore(); { std::lock_guard lock(store.completedMutex); ready.swap(store.completed); } bool handledAny = false; while (!ready.empty()) { DeferredDnsCompletion completion = std::move(ready.front()); ready.pop_front(); const DeferredNetworkRoute& route = completion.work.route; if (route.kind == DeferredNetworkCompletionKind::SyncWaitQueue && !DeferredNetworkWaiterIsValid(route)) { continue; } int32_t result = completion.result; if (result == 0) { try { result = ApplyDeferredDnsCompletion(completion); } catch (const Memory::AccessViolation& error) { const std::string_view reason = error.reason(); NetFail("dns %s '%s' result write faulted: %.*s -> wii=%d", DeferredDnsKindName(completion.work.kind), completion.work.node.c_str(), static_cast(reason.size()), reason.data(), -SO_EINVAL); result = -SO_EINVAL; } } DeliverDeferredNetworkResult(cpu, store, route, result); handledAny = true; } const auto now = NetworkPollContract::Timeout::Clock::now(); for (auto it = store.pendingPolls.begin(); it != store.pendingPolls.end();) { DeferredPollWork& work = *it; const DeferredNetworkRoute route = work.route; if (route.kind == DeferredNetworkCompletionKind::SyncWaitQueue && !DeferredNetworkWaiterIsValid(route)) { it = store.pendingPolls.erase(it); continue; } const std::optional probedResult = ProbeDeferredPoll(work, now); if (!probedResult) { ++it; continue; } int32_t result = *probedResult; const size_t descriptorCount = work.descriptors.size(); try { result = ApplyDeferredPollCompletion(work, result); } catch (const Memory::AccessViolation& error) { const std::string_view reason = error.reason(); NetFail("SO_POLL(wait) result write faulted for %llu descriptor(s): %.*s -> wii=%d", static_cast(descriptorCount), static_cast(reason.size()), reason.data(), -SO_EINVAL); result = -SO_EINVAL; } it = store.pendingPolls.erase(it); DeliverDeferredNetworkResult(cpu, store, route, result); handledAny = true; } for (auto it = store.pendingConnects.begin(); it != store.pendingConnects.end();) { DeferredConnectWork& work = *it; const DeferredNetworkRoute route = work.route; if (route.kind == DeferredNetworkCompletionKind::SyncWaitQueue && !DeferredNetworkWaiterIsValid(route)) { it = store.pendingConnects.erase(it); continue; } const std::optional probedResult = ProbeDeferredConnect(work, now); if (!probedResult) { ++it; continue; } int32_t result = *probedResult; if (result == 0) { if (DeferredConnectSocketIsStillValid(work)) { WiiSocket& socket = g_sockets[work.wiiFd]; socket.peerPort = ntohs(work.peerAddress.sin_port); socket.peerAddr = work.peerAddress; socket.hasPeerAddr = true; } else { result = -SO_EBADF; } } g_lastSocketError = result; // Every blocking connect settles here exactly once, whether it was // resolved at submission, probed, timed out or aborted by a close - so // this is the single place a failed WFC connect becomes visible. if (result != 0) { char peer[INET_ADDRSTRLEN]{}; inet_ntop(AF_INET, &work.peerAddress.sin_addr, peer, sizeof(peer)); NetFail("SO_CONNECT fd=%u -> %s:%u FAILED wii=%d", work.wiiFd, peer[0] ? peer : "0.0.0.0", static_cast(ntohs(work.peerAddress.sin_port)), result); } it = store.pendingConnects.erase(it); DeliverDeferredNetworkResult(cpu, store, route, result); handledAny = true; } return handledAny; }