Merge pull request #22945 from hrydgard/net-hardening

Web server, HTTP client, inet: Check Origin and bound what the network can make us do
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-11-05 19:41:22 +01:00
commit f82a875d17
21 files changed
+277 -27

No files matched your search

+6
View File
@@ -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);
+12 -2
View File
@@ -499,8 +499,13 @@ int Client::ReadResponseEntity(net::Buffer *readbuf, const std::vector<std::stri
// Just sanity checking...
contentLength = 0;
}
if ((size_t)contentLength > 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::vector<std::stri
if (gzip) {
std::string compressed, decompressed;
output->TakeAll(&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.
+6
View File
@@ -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.
+27 -6
View File
@@ -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++;
+7
View File
@@ -35,6 +35,7 @@ struct NaettBodySink {
std::string postData;
// Written by us, read by the transfer thread.
std::atomic<bool> 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<naettOption *> options;
options.push_back(naettMethod(method_ == RequestMethod::GET ? "GET" : "POST"));
+7
View File
@@ -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,
+19 -2
View File
@@ -13,6 +13,7 @@
#else
#include <sys/select.h> /* FD_SETSIZE */
#include <sys/socket.h> /* socket definitions */
#include <sys/types.h> /* socket types */
#include <sys/wait.h> /* for waitpid() */
@@ -43,10 +44,14 @@
#include "Common/Log.h"
void NewThreadExecutor::Run(std::function<void()> func) {
bool NewThreadExecutor::Run(std::function<void()> 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<std::atomic<bool>>(false);
Worker worker;
@@ -56,6 +61,7 @@ void NewThreadExecutor::Run(std::function<void()> 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 {
+4 -1
View File
@@ -13,7 +13,10 @@
class NewThreadExecutor {
public:
~NewThreadExecutor();
void Run(std::function<void()> func);
// Returns false, without running func, when MAX_WORKERS threads are already busy.
bool Run(std::function<void()> func);
static constexpr size_t MAX_WORKERS = 64;
private:
// Reap threads that have finished. Only called from the thread that calls Run().
+6 -2
View File
@@ -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<char> 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;
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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<std::string_view, std::string_view> BufferParts() const;
void Fill();
bool Block();
void AccountDrain(size_t bytes);
size_t FindNewline() const;
+31 -2
View File
@@ -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);
+4
View File
@@ -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;
+56
View File
@@ -16,11 +16,13 @@
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <cctype>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <vector>
#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;
+2
View File
@@ -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);
+2 -1
View File
@@ -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;
}
+4
View File
@@ -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;
+52
View File
@@ -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::vector<HostIovec
SetInetErrno(ERROR_INET_EINVAL, func, threadID);
return false;
}
// The BSD stack's limit (UIO_MAXIOV). Otherwise only guest memory bounds it, and a blocking
// recvmsg rebuilds the list on every retry.
if (msg->msg_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)) ||
+7 -2
View File
@@ -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);
+9 -5
View File
@@ -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);
+12 -2
View File
@@ -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).