diff --git a/Common/File/FileDescriptor.cpp b/Common/File/FileDescriptor.cpp index 61fa4a73e8..8a365a4ec0 100644 --- a/Common/File/FileDescriptor.cpp +++ b/Common/File/FileDescriptor.cpp @@ -13,6 +13,12 @@ namespace fd_util { bool WaitUntilReady(int fd, double timeout, bool for_write) { +#if !PPSSPP_PLATFORM(WINDOWS) + // FD_SET on an fd past the end of the set writes off the end of it, on the stack. + if (fd < 0 || fd >= FD_SETSIZE) { + return false; + } +#endif struct timeval tv; tv.tv_sec = (long)floor(timeout); tv.tv_usec = (long)((timeout - floor(timeout)) * 1000000.0); diff --git a/Common/Net/HTTPClient.cpp b/Common/Net/HTTPClient.cpp index 97d73d2369..f0de161571 100644 --- a/Common/Net/HTTPClient.cpp +++ b/Common/Net/HTTPClient.cpp @@ -499,8 +499,13 @@ int Client::ReadResponseEntity(net::Buffer *readbuf, const std::vector maxResponseSize_) { + ERROR_LOG(Log::HTTP, "Response too large: %d bytes", contentLength); + return -1; + } - if (!readbuf->ReadAllWithProgress(sock(), contentLength, progress)) + // readbuf may already hold the start of the body, read along with the headers. + if (readbuf->size() > maxResponseSize_ || !readbuf->ReadAllWithProgress(sock(), contentLength, progress, maxResponseSize_ - readbuf->size())) return -1; // output now contains the rest of the reply. Dechunk it. @@ -521,7 +526,7 @@ int Client::ReadResponseEntity(net::Buffer *readbuf, const std::vectorTakeAll(&compressed); - bool result = decompress_string(compressed, &decompressed); + bool result = decompress_string(compressed, &decompressed, maxResponseSize_); if (!result) { ERROR_LOG(Log::HTTP, "Error decompressing using zlib"); if (progress) { @@ -581,6 +586,7 @@ int HTTPRequest::Perform(const std::string &url) { if (!userAgent_.empty()) { client.SetUserAgent(userAgent_); } + client.SetMaxResponseSize(outfile_.empty() ? MAX_RESPONSE_IN_MEMORY : MAX_RESPONSE_TO_FILE); if (!client.Resolve(fileUrl.Host().c_str(), fileUrl.Port())) { ERROR_LOG(Log::HTTP, "Failed resolving %s", url.c_str()); @@ -625,6 +631,7 @@ void HTTPRequest::Do() { resultCode_ = 0; std::string downloadURL = url_; + int redirects = 0; while (resultCode_ == 0) { // This is where the new request is performed. int resultCode = Perform(downloadURL); @@ -641,6 +648,9 @@ void HTTPRequest::Do() { } else if (redirectURL == downloadURL || redirectURL == url_) { // Simple loop detected, bail out. resultCode_ = resultCode; + } else if (++redirects > MAX_REDIRECTS) { + ERROR_LOG(Log::HTTP, "Too many redirects requesting %s", url_.c_str()); + resultCode_ = resultCode; } // Perform the next GET. diff --git a/Common/Net/HTTPClient.h b/Common/Net/HTTPClient.h index 65f61e0720..0d875a90c2 100644 --- a/Common/Net/HTTPClient.h +++ b/Common/Net/HTTPClient.h @@ -98,10 +98,16 @@ public: httpVersion_ = version; } + // The most body ReadResponseEntity accepts, before and after decompression. + void SetMaxResponseSize(size_t size) { + maxResponseSize_ = size; + } + protected: std::string userAgent_; const char* httpVersion_; double headerTimeout_ = 900.0; + size_t maxResponseSize_ = MAX_RESPONSE_IN_MEMORY; }; // Really an asynchronous request. diff --git a/Common/Net/HTTPHeaders.cpp b/Common/Net/HTTPHeaders.cpp index bbd3a3fcfd..4fccbe4c68 100644 --- a/Common/Net/HTTPHeaders.cpp +++ b/Common/Net/HTTPHeaders.cpp @@ -9,9 +9,19 @@ #include "Common/Log.h" #include "Common/File/FileDescriptor.h" #include "Common/StringUtils.h" +#include "Common/TimeUtil.h" namespace http { +// Limits on a request's header block, so a client can't hold a connection or grow memory forever. +static constexpr int MAX_HEADER_LINES = 100; +static constexpr size_t MAX_HEADER_BYTES = 64 * 1024; +static constexpr double MAX_HEADER_SECONDS = 10.0; + +static bool KeyIs(const char *key, int key_len, const char *name) { + return key_len == (int)strlen(name) && !strncasecmp(key, name, key_len); +} + RequestHeader::RequestHeader() { } @@ -58,13 +68,13 @@ int RequestHeader::ParseHttpHeader(const char *buffer) { if (first_header_) { // Step 1: Method first_header_ = false; - if (!memcmp(buffer, "GET ", 4)) { + if (!strncmp(buffer, "GET ", 4)) { method = GET; buffer += 4; - } else if (!memcmp(buffer, "HEAD ", 5)) { + } else if (!strncmp(buffer, "HEAD ", 5)) { method = HEAD; buffer += 5; - } else if (!memcmp(buffer, "POST ", 5)) { + } else if (!strncmp(buffer, "POST ", 5)) { method = POST; buffer += 5; } else { @@ -133,14 +143,16 @@ int RequestHeader::ParseHttpHeader(const char *buffer) { SkipSpace(&buffer); int value_len = (int)strlen(buffer); - if (!strncasecmp(key, "User-Agent", key_len)) { + if (KeyIs(key, key_len, "User-Agent")) { + delete[] user_agent; user_agent = new char[value_len + 1]; memcpy(user_agent, buffer, value_len + 1); VERBOSE_LOG(Log::HTTP, "user-agent: %s", user_agent); - } else if (!strncasecmp(key, "Referer", key_len)) { + } else if (KeyIs(key, key_len, "Referer")) { + delete[] referer; referer = new char[value_len + 1]; memcpy(referer, buffer, value_len + 1); - } else if (!strncasecmp(key, "Content-Length", key_len)) { + } else if (KeyIs(key, key_len, "Content-Length")) { content_length = atoi(buffer); VERBOSE_LOG(Log::HTTP, "Content-Length: %i", (int)content_length); } else { @@ -154,12 +166,21 @@ int RequestHeader::ParseHttpHeader(const char *buffer) { void RequestHeader::ParseHeaders(net::InputSink *sink) { int line_count = 0; + size_t total_bytes = 0; + const double deadline = time_now_d() + MAX_HEADER_SECONDS; std::string line; while (sink->ReadLine(line)) { if (line.length() == 0) { // Blank line, this means end of headers. break; } + total_bytes += line.length(); + if (line_count >= MAX_HEADER_LINES || total_bytes > MAX_HEADER_BYTES || time_now_d() > deadline) { + WARN_LOG(Log::HTTP, "Request headers too long, dropping the request"); + status = 431; + ok = false; + return; + } ParseHttpHeader(line.c_str()); line_count++; diff --git a/Common/Net/HTTPNaettRequest.cpp b/Common/Net/HTTPNaettRequest.cpp index b2991274f2..50150769ef 100644 --- a/Common/Net/HTTPNaettRequest.cpp +++ b/Common/Net/HTTPNaettRequest.cpp @@ -35,6 +35,7 @@ struct NaettBodySink { std::string postData; // Written by us, read by the transfer thread. std::atomic cancelled{false}; + size_t maxBytes = MAX_RESPONSE_IN_MEMORY; }; // Sinks belonging to requests that hadn't finished when they were torn down, which only happens @@ -62,6 +63,11 @@ int HTTPSRequest::WriteBodyThunk(const void *source, int bytes, void *userData) if (bytes <= 0) { return 0; } + if (sink->buffer.size() + (size_t)bytes > sink->maxBytes) { + // Fails the request, like cancelling does. + ERROR_LOG(Log::HTTP, "Response larger than %lld bytes, giving up", (long long)sink->maxBytes); + return 0; + } char *dest = sink->buffer.Append((size_t)bytes); memcpy(dest, source, bytes); return bytes; @@ -84,6 +90,7 @@ void HTTPSRequest::Start() { // In case someone managed to cancel us between construction and here. sink_->cancelled = cancelled_; sink_->postData = postData_; + sink_->maxBytes = outfile_.empty() ? MAX_RESPONSE_IN_MEMORY : MAX_RESPONSE_TO_FILE; std::vector options; options.push_back(naettMethod(method_ == RequestMethod::GET ? "GET" : "POST")); diff --git a/Common/Net/HTTPRequest.h b/Common/Net/HTTPRequest.h index 0cd8db3288..0bd3885600 100644 --- a/Common/Net/HTTPRequest.h +++ b/Common/Net/HTTPRequest.h @@ -9,6 +9,13 @@ namespace http { +// Caps on a response body, so a server can't make us buffer until we run out of memory. Downloads +// to a file are buffered in memory too, so they get a cap of their own. +constexpr size_t MAX_RESPONSE_IN_MEMORY = 64 * 1024 * 1024; +constexpr size_t MAX_RESPONSE_TO_FILE = 1024 * 1024 * 1024; +// Redirects we follow before giving up. +constexpr int MAX_REDIRECTS = 10; + enum class RequestMethod { GET, POST, diff --git a/Common/Net/HTTPServer.cpp b/Common/Net/HTTPServer.cpp index 04db0acf9c..ecc03c07cb 100644 --- a/Common/Net/HTTPServer.cpp +++ b/Common/Net/HTTPServer.cpp @@ -13,6 +13,7 @@ #else +#include /* FD_SETSIZE */ #include /* socket definitions */ #include /* socket types */ #include /* for waitpid() */ @@ -43,10 +44,14 @@ #include "Common/Log.h" -void NewThreadExecutor::Run(std::function func) { +bool NewThreadExecutor::Run(std::function func) { // Every connection gets a thread, and we only ever joined them at shutdown - so a server that // had served N connections was still holding N joinable std::threads. Reap the finished ones. Prune(); + if (workers_.size() >= MAX_WORKERS) { + // Otherwise anyone who can connect gets a thread per connection, until creating one throws. + return false; + } auto done = std::make_shared>(false); Worker worker; @@ -56,6 +61,7 @@ void NewThreadExecutor::Run(std::function func) { done->store(true, std::memory_order_release); }); workers_.push_back(std::move(worker)); + return true; } void NewThreadExecutor::Prune() { @@ -328,7 +334,18 @@ bool Server::RunSlice(double timeout) { socklen_t client_addr_size = sizeof(client_addr); int conn_fd = accept(listenerSock_, &client_addr.sa, &client_addr_size); if (conn_fd >= 0) { - executor_->Run(std::bind(&Server::HandleConnection, this, conn_fd)); +#if !PPSSPP_PLATFORM(WINDOWS) + // The sinks and the WebSocket server wait on the socket with select(). + if (conn_fd >= FD_SETSIZE) { + WARN_LOG(Log::HTTP, "Too many open files, dropping connection"); + closesocket(conn_fd); + return true; + } +#endif + if (!executor_->Run(std::bind(&Server::HandleConnection, this, conn_fd))) { + WARN_LOG(Log::HTTP, "Too many connections, dropping one"); + closesocket(conn_fd); + } return true; } else { diff --git a/Common/Net/HTTPServer.h b/Common/Net/HTTPServer.h index b6d1be7fc9..f235c6fc54 100644 --- a/Common/Net/HTTPServer.h +++ b/Common/Net/HTTPServer.h @@ -13,7 +13,10 @@ class NewThreadExecutor { public: ~NewThreadExecutor(); - void Run(std::function func); + // Returns false, without running func, when MAX_WORKERS threads are already busy. + bool Run(std::function func); + + static constexpr size_t MAX_WORKERS = 64; private: // Reap threads that have finished. Only called from the thread that calls Run(). diff --git a/Common/Net/NetBuffer.cpp b/Common/Net/NetBuffer.cpp index 24a649745a..4874b39c5c 100644 --- a/Common/Net/NetBuffer.cpp +++ b/Common/Net/NetBuffer.cpp @@ -58,7 +58,7 @@ bool Buffer::FlushSocket(uintptr_t sock, double timeout, bool *cancelled) { return true; } -bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress) { +bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress, size_t maxBytes) { static constexpr float CANCEL_INTERVAL = 0.25f; std::vector buf; // We're non-blocking and reading from an OS buffer, so try to read as much as we can at a time. @@ -71,7 +71,7 @@ bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progres } double st = time_now_d(); - int total = 0; + int64_t total = 0; while (true) { bool ready = false; @@ -95,6 +95,10 @@ bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progres // Just try again on a would block error, not a real error. continue; } + if ((uint64_t)total + retval > maxBytes) { + ERROR_LOG(Log::IO, "Response larger than %lld bytes, giving up", (long long)maxBytes); + return false; + } char *p = Append((size_t)retval); memcpy(p, &buf[0], retval); total += retval; diff --git a/Common/Net/NetBuffer.h b/Common/Net/NetBuffer.h index 7fe8dc6d5f..271ea9d38a 100644 --- a/Common/Net/NetBuffer.h +++ b/Common/Net/NetBuffer.h @@ -24,7 +24,8 @@ public: bool FlushSocket(uintptr_t sock, double timeout, bool *cancelled = nullptr); // If you know the size of the file to read, pass it in knownSize, for best performance and for progress reporting. - bool ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress); + // Fails once more than maxBytes have arrived. + bool ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress, size_t maxBytes); // < 0: error // >= 0: number of bytes read diff --git a/Common/Net/Sinks.h b/Common/Net/Sinks.h index 4fd621b03e..f5725a4824 100644 --- a/Common/Net/Sinks.h +++ b/Common/Net/Sinks.h @@ -27,6 +27,8 @@ public: bool Empty() const; bool TryFill(); + // Waits (up to a few seconds) for more data to arrive. False on timeout, error or end of stream. + bool Block(); bool HasError() const { return hasError_; } // True once the peer has closed its end. Sticky - no more data can ever arrive, which is not // the same as "nothing right now", and callers that wait for more need to tell them apart. @@ -44,7 +46,6 @@ public: private: std::pair BufferParts() const; void Fill(); - bool Block(); void AccountDrain(size_t bytes); size_t FindNewline() const; diff --git a/Common/Net/WebsocketServer.cpp b/Common/Net/WebsocketServer.cpp index 0a9323feb0..bf667ec566 100644 --- a/Common/Net/WebsocketServer.cpp +++ b/Common/Net/WebsocketServer.cpp @@ -12,6 +12,7 @@ #include "Common/Crypto/sha1.h" #include "Common/Log.h" #include "Common/StringUtils.h" +#include "Common/TimeUtil.h" static const char *const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; @@ -19,6 +20,10 @@ static const char *const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" // claiming an enormous frame length before sending any actual data. // TODO: Make configurable? static constexpr uint64_t MAX_WS_MESSAGE_SIZE = 128 * 1024 * 1024; +// How much we queue for a client that isn't reading, before giving up on it. +static constexpr size_t MAX_WS_OUT_BACKLOG = 128 * 1024 * 1024; +// How long we wait for our close frame to go out. +static constexpr double WS_CLOSE_TIMEOUT_SECONDS = 2.0; namespace net { @@ -214,6 +219,7 @@ void WebSocketServer::Close(WebSocketClose reason) { } sentClose_ = true; + sentCloseTime_ = time_now_d(); } // The connection is gone or unusable - there's nothing left to send and no point waiting for @@ -247,6 +253,11 @@ bool WebSocketServer::Process(float timeout) { open_ = false; return false; } + if (sentClose_ && time_now_d() - sentCloseTime_ > WS_CLOSE_TIMEOUT_SECONDS) { + // The peer isn't reading, so the close frame is never going out. + CloseAbnormally(); + return false; + } struct timeval tv; tv.tv_sec = floor(timeout); @@ -437,11 +448,14 @@ bool WebSocketServer::ReadFrame() { } bool WebSocketServer::ReadPending() { + // Grow by at most what one read can return, not by the length the frame claims: a client could + // otherwise make us commit (and zero, on every lap) MAX_WS_MESSAGE_SIZE without sending it. size_t pos = pendingBuf_.size(); - pendingBuf_.resize(pendingBuf_.size() + pendingLeft_); + size_t chunk = (size_t)std::min(pendingLeft_, (uint64_t)InputSink::BUFFER_SIZE); + pendingBuf_.resize(pos + chunk); // Read what we can. - size_t readBytes = in_->TakeAtMost((char *)&pendingBuf_[pos], pendingLeft_); + size_t readBytes = in_->TakeAtMost((char *)&pendingBuf_[pos], chunk); for (size_t i = 0; i < readBytes; ++i) { pendingBuf_[pos + i] ^= pendingMask_[i & 3]; } @@ -599,6 +613,9 @@ void WebSocketServer::SendHeader(bool fin, int opcode, size_t sz) { } void WebSocketServer::SendBytes(const void *p, size_t sz) { + if (outputOverflowed_) { + return; + } const char *data = (const char *)p; if (OutBufPending() == 0) { size_t pushed = out_->PushAtMost(data, sz); @@ -607,6 +624,18 @@ void WebSocketServer::SendBytes(const void *p, size_t sz) { } if (sz != 0) { + if (OutBufPending() + sz > MAX_WS_OUT_BACKLOG) { + // The client stopped reading. Drop the connection rather than buffer without limit. + ERROR_LOG(Log::IO, "WebSocket client isn't reading, dropping it"); + outputOverflowed_ = true; + closeReason_ = WebSocketClose::ABNORMAL; + sentClose_ = true; + sentCloseTime_ = time_now_d(); + outBuf_.clear(); + outBufOffset_ = 0; + out_->Discard(); + return; + } size_t pos = outBuf_.size(); outBuf_.resize(pos + sz); memcpy(&outBuf_[pos], data, sz); diff --git a/Common/Net/WebsocketServer.h b/Common/Net/WebsocketServer.h index 398fa32dff..a35f261aff 100644 --- a/Common/Net/WebsocketServer.h +++ b/Common/Net/WebsocketServer.h @@ -90,6 +90,10 @@ protected: bool open_ = true; bool sentClose_ = false; + // When sentClose_ was set. A peer that never reads would otherwise keep us waiting forever. + double sentCloseTime_ = 0.0; + // Set when the peer stopped reading and the backlog hit its cap. Nothing more gets queued. + bool outputOverflowed_ = false; int fragmentOpcode_ = -1; size_t fd_ = 0; InputSink *in_ = nullptr; diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp index adfc515c23..0d0a725a02 100644 --- a/Core/Debugger/WebSocket.cpp +++ b/Core/Debugger/WebSocket.cpp @@ -16,11 +16,13 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include #include #include #include +#include "Common/StringUtils.h" #include "Common/Thread/ThreadUtil.h" #include "Common/TimeUtil.h" #include "Core/Core.h" @@ -226,9 +228,63 @@ void WebSocketDebuggerTick() { } } +// Browsers let any page open a WebSocket to localhost, and the debugger can write files and memory. +// So only accept pages from this machine, from this server (the bundled UI), or the official web +// debugger. Non-browser clients don't send an Origin. +static bool IsAllowedOrigin(const http::ServerRequest &request) { + std::string origin; + if (!request.GetHeader("origin", &origin)) { + return true; + } + auto toLower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return (char)tolower(c); }); + return s; + }; + origin = toLower(origin); + + size_t schemeEnd = origin.find("://"); + if (schemeEnd == std::string::npos) { + // Includes "null", sent by file:// pages and sandboxed frames. + return false; + } + const std::string scheme = origin.substr(0, schemeEnd); + if (scheme != "http" && scheme != "https") { + return false; + } + const std::string hostPort = origin.substr(schemeEnd + 3); + if (hostPort == "ppsspp-debugger.unknownbrackets.org") { + return true; + } + + std::string hostname = hostPort; + if (startsWith(hostname, "[")) { + hostname = hostname.substr(0, hostname.find(']') + 1); + } else { + hostname = hostname.substr(0, hostname.find(':')); + } + if (hostname == "localhost" || hostname == "127.0.0.1" || hostname == "[::1]") { + return true; + } + + // A name matching Host proves nothing: an attacker's domain can be made to resolve to us (DNS + // rebinding). So only an IP address, the way the bundled UI is reached over the LAN. + const bool isIP = startsWith(hostname, "[") || (!hostname.empty() && hostname.find_first_not_of("0123456789.") == std::string::npos); + std::string host; + return isIP && request.GetHeader("host", &host) && toLower(host) == hostPort; +} + void HandleDebuggerRequest(const http::ServerRequest &request) { SetCurrentThreadName("WebSocketDebugger"); + if (!IsAllowedOrigin(request)) { + std::string origin; + request.GetHeader("origin", &origin); + WARN_LOG(Log::Debugger, "Rejected debugger connection from origin '%s'", origin.c_str()); + request.WriteHttpResponseHeader("1.1", 403, -1, "text/plain"); + request.Out()->Push("Origin not allowed."); + return; + } + net::WebSocketServer *ws = net::WebSocketServer::CreateAsUpgrade(request, "debugger.ppsspp.org"); if (!ws) { return; diff --git a/Core/FileLoaders/HTTPFileLoader.cpp b/Core/FileLoaders/HTTPFileLoader.cpp index c41a9c80b6..ee60d7a856 100644 --- a/Core/FileLoaders/HTTPFileLoader.cpp +++ b/Core/FileLoaders/HTTPFileLoader.cpp @@ -244,6 +244,8 @@ size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags f // TODO: Would be nice to read directly. net::Buffer output; + // We only use what we asked for, so don't let the server send more than that. + client_.SetMaxResponseSize((size_t)(absoluteEnd - absolutePos)); int res = client_.ReadResponseEntity(&readbuf, responseHeaders, &output, &progress_); if (res != 0) { ERROR_LOG(Log::Loader, "Unable to read HTTP response entity: %d", res); diff --git a/Core/HLE/SocketManager.cpp b/Core/HLE/SocketManager.cpp index 85dffeb311..5f8aef07a4 100644 --- a/Core/HLE/SocketManager.cpp +++ b/Core/HLE/SocketManager.cpp @@ -16,7 +16,8 @@ InetSocket *SocketManager::CreateSocket(int *index, int *returned_errno, SocketS int hostProtocol = convertSocketProtoPSP2Host(protocol); SOCKET hostSock = ::socket(hostDomain, hostType, hostProtocol); - if (hostSock < 0) { + // SOCKET is unsigned on Windows, so not < 0. + if (hostSock == INVALID_SOCKET) { *returned_errno = socket_errno; return nullptr; } diff --git a/Core/HLE/sceNet.cpp b/Core/HLE/sceNet.cpp index 193fd934c6..b7b6f8789d 100644 --- a/Core/HLE/sceNet.cpp +++ b/Core/HLE/sceNet.cpp @@ -305,6 +305,10 @@ bool LoadDNSForGameID(std::string_view gameID, std::string_view jsonStr, InfraDN if (game.hasChild("domains", JSON_OBJECT)) { const JsonGet domains = game.getDict("domains"); for (const auto &iter : domains.value_) { + // toString() on anything else isn't a string pointer. + if (iter->value.getTag() != JSON_STRING) { + continue; + } std::string domain = std::string(iter->key); std::string ipAddr = std::string(iter->value.toString()); dns->fixedDNS[domain] = ipAddr; diff --git a/Core/HLE/sceNetInet.cpp b/Core/HLE/sceNetInet.cpp index c66a5a33f1..be33a50846 100644 --- a/Core/HLE/sceNetInet.cpp +++ b/Core/HLE/sceNetInet.cpp @@ -922,6 +922,46 @@ static bool StartInetWait(InetWaitOp op, const u32 *args, int numArgs, s64 timeo return true; } +// The entry points check the arguments before a wait starts, but a wait can also come from a +// savestate. Same checks, failing with EFAULT (errno set) like the entry points would. +static bool InetWaitArgsValid(const InetWait &wait, SceUID threadID) { + const u32 *args = wait.args; + const char *func = "inet wait"; + auto optional = [&](u32 ptr, u32 size) { + return ptr == 0 || InetValidRange(ptr, size, func, threadID); + }; + switch (wait.op) { + case InetWaitOp::Select: + if ((int)args[0] < 0 || (int)args[0] > PSP_NET_INET_FD_SETSIZE) { + SetInetErrno(ERROR_INET_EINVAL, func, threadID); + return false; + } + return optional(args[1], sizeof(SceNetInetFdSet)) && optional(args[2], sizeof(SceNetInetFdSet)) && optional(args[3], sizeof(SceNetInetFdSet)); + case InetWaitOp::Poll: + if (args[1] > PSP_NET_INET_FD_SETSIZE) { + SetInetErrno(ERROR_INET_EINVAL, func, threadID); + return false; + } + return InetValidRange(args[0], args[1] * (u32)sizeof(SceNetInetPollfd), func, threadID); + case InetWaitOp::Recv: + case InetWaitOp::Send: + return InetValidRange(args[1], args[2], func, threadID); + case InetWaitOp::RecvFrom: + return InetValidRange(args[1], args[2], func, threadID) && optional(args[4], sizeof(SceNetInetSockaddr)) && optional(args[5], 4); + case InetWaitOp::SendTo: + return InetValidRange(args[1], args[2], func, threadID) && optional(args[4], sizeof(SceNetInetSockaddr)); + case InetWaitOp::Accept: + return optional(args[1], sizeof(SceNetInetSockaddr)) && optional(args[2], 4); + case InetWaitOp::Connect: + case InetWaitOp::RecvMsg: + // Connect has no pointers, and TryRecvMsg checks its msghdr on every attempt. + return true; + default: + SetInetErrno(ERROR_INET_EINVAL, func, threadID); + return false; + } +} + static void __NetInetWaitPoll(u64 userdata, int cyclesLate) { const SceUID threadID = (SceUID)userdata; auto it = g_inetWaits.find(threadID); @@ -936,6 +976,12 @@ static void __NetInetWaitPoll(u64 userdata, int cyclesLate) { } const InetWait wait = it->second; + if (!InetWaitArgsValid(wait, threadID)) { + g_inetWaits.erase(it); + __KernelResumeThreadFromWait(threadID, -1); + __KernelReSchedule("inet wait failed"); + return; + } const InetTry attempt = TryInetOp(wait.op, threadID, wait.args, false); const s64 now = CoreTiming::GetTicks(currentMIPS); if (attempt.wouldBlock && (wait.deadline < 0 || now < wait.deadline)) { @@ -1964,6 +2010,12 @@ static bool ReadInetMsghdr(u32 msghdrPtr, InetMsghdr *msg, std::vectormsg_iovlen > 1024) { + SetInetErrno(ERROR_INET_EMSGSIZE, func, threadID); + return false; + } // In 64 bits, so a huge count can't wrap around to a small size. const u64 iovBytes = (u64)msg->msg_iovlen * sizeof(SceNetIovec); if ((msg->msg_name && !InetValidRange(msg->msg_name, sizeof(SceNetInetSockaddr), func, threadID)) || diff --git a/Core/HLE/sceNetResolver.cpp b/Core/HLE/sceNetResolver.cpp index 00a03a35aa..cbe8826789 100644 --- a/Core/HLE/sceNetResolver.cpp +++ b/Core/HLE/sceNetResolver.cpp @@ -102,7 +102,9 @@ static int CheckNtoAArgs(u32 hostnamePtr, u32 inAddrPtr) { // Note: timeouts are in seconds static int NetResolver_StartNtoA(NetResolver *resolver, u32 hostnamePtr, u32 inAddrPtr, int timeout, int retry) { - std::string hostname = std::string(safe_string(Memory::GetCharPointer(hostnamePtr))); + // CheckNtoAArgs checked the address. The name needn't be terminated before the end of memory. + const char *hostnameChars = Memory::GetCharPointerUnchecked(hostnamePtr); + std::string hostname(hostnameChars, strnlen(hostnameChars, Memory::MaxSizeAtAddress(hostnamePtr))); // Process hostname with infra-DNS std::string processedHostname = ProcessHostnameWithInfraDNS(hostname); @@ -154,7 +156,10 @@ static int sceNetResolverStartNtoA(int resolverId, u32 hostnamePtr, u32 inAddrPt return hleLogError(Log::sceNet, error); } - for (int attempt = 0; attempt < retry; ++attempt) { + // Each attempt blocks the emulator for as long as the host's lookups take (seconds against a dead + // DNS server), and the game's count isn't limited, so a couple will do. + const int attempts = std::min(retry, 2); + for (int attempt = 0; attempt < attempts; ++attempt) { const int status = NetResolver_StartNtoA(&iter->second, hostnamePtr, inAddrPtr, timeout, retry); if (status >= 0) { return hleDelayResult(hleLogDebugOrError(Log::sceNet, status), "resolver lookup", RESOLVER_LOOKUP_US); diff --git a/Core/WebServer.cpp b/Core/WebServer.cpp index 5eb4c2ffb3..431a47f4cd 100644 --- a/Core/WebServer.cpp +++ b/Core/WebServer.cpp @@ -310,7 +310,10 @@ static void DiscHandler(const http::ServerRequest &request, const Path &filename s64 chunklen = std::min(len - pos, (s64)CHUNK_SIZE); if (fread(buf, chunklen, 1, fp) != 1) break; - request.Out()->Push(buf, chunklen); + if (!request.Out()->Push(buf, chunklen)) { + // The client stopped reading or went away. Each failed Push already waited a while. + break; + } } fclose(fp); delete[] buf; @@ -679,10 +682,11 @@ static MultiPartResult HandleMultipartPart(const http::ServerRequest &request, s } progress.AddBytes(readBytes); bytesTransferred += readBytes; - if (readBytes == 0 && !terminatorFound && (request.In()->AtEnd() || request.In()->HasError())) { - // Peer went away mid-upload (a cancelled browser upload, say). Without this the loop - // spins forever, which also blocks web server shutdown since it joins its threads. - ERROR_LOG(Log::HTTP, "Connection closed during upload of '%s'", filename.c_str()); + // ReadBinaryUntilTerminator doesn't wait, so wait here for more data when there was none. + if (readBytes == 0 && !terminatorFound && !request.In()->Block()) { + // Peer went away mid-upload (a cancelled browser upload, say), or stopped sending. Without + // this the loop spins forever, which also blocks web server shutdown since it joins its threads. + ERROR_LOG(Log::HTTP, "Connection closed or timed out during upload of '%s'", filename.c_str()); if (fp) { fclose(fp); File::Delete(destPath); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index d4409e2b9e..0055c34733 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -34,8 +34,18 @@ This doc is a local reference, the user-facing documentation is on the website. to `/debugger` without a websocket Upgrade just redirects to the bundled web UI at `/debugger/index.html`). - Messages are JSON, both directions, always shaped as `{"event": "NAME", ...}`. -- One WebSocket connection = one client; PPSSPP does not limit the number of - simultaneous debugger connections. +- One WebSocket connection = one client. The web server as a whole handles at + most 64 connections at a time (`NewThreadExecutor::MAX_WORKERS`), debugger + connections included. +- Browser pages are checked by their `Origin` header, since any page can open a + WebSocket to localhost and the debugger can write memory and files. Allowed: + no `Origin` at all (native clients, wsdbg, scripts), pages served from + `localhost`, `127.0.0.1` or `[::1]` on any port, pages served by this server + itself by IP address (the `Origin` matches `Host`, as for the bundled UI; a + hostname doesn't count, since DNS rebinding can point one at us), and + `ppsspp-debugger.unknownbrackets.org`. Anything else gets a 403. +- A client that stops reading gets dropped once 128 MB of responses are queued + for it. - The debugger only actually does anything while `WebServerFlags::DEBUGGER` is enabled (see "Enabling it" below) - the HTTP server itself may also be running for other reasons (Remote ISO, upload).