P1: address review (fail-open hardening)

This commit is contained in:
Guilherme Fontes
2026-07-18 03:29:37 +01:00
parent b4ae5c1b34
commit 86a4660e41
+52 -18
View File
@@ -13,6 +13,7 @@
#include <netdb.h>
#include <netinet/in.h>
#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <nlohmann/json.hpp>
#if __APPLE__
@@ -75,10 +76,11 @@ static bool allSupportedLocally(Store & store, const std::set<std::string>& requ
* (machineHasRoom -> true, liveLoadPenalty -> 0).
* ------------------------------------------------------------------------ */
// Default estimate for a drv that does not match the heavy-crate table.
// "Light" means: we have NO confident signal that this drv is memory-heavy,
// so machineHasRoom never filters on account of it.
static const uint64_t adaptiveLightMib = 512;
// A drv that does not match the heavy-crate table is treated as "light":
// we have NO confident signal that it is memory-heavy, so adaptiveEstPeakRSS
// returns nullopt and machineHasRoom never filters on account of it. (There is
// deliberately no numeric light default - an unmatched drv must always permit,
// so keying it on a free-RAM threshold would wrongly filter light drvs.)
// name-substring -> estimated peak RSS in MiB (from LIX_ADAPTIVE_RSS_TABLE).
static std::map<std::string, uint64_t> adaptiveRssTable;
@@ -108,8 +110,12 @@ static void adaptiveLoadConfig()
f >> j;
if (j.is_object())
for (auto & [k, v] : j.items())
if (v.is_number())
adaptiveRssTable[k] = v.get<uint64_t>();
// Per-entry guard: one bad value skips only that entry,
// it does not discard the whole (otherwise valid) table.
try {
if (v.is_number_unsigned() || (v.is_number_integer() && v.get<int64_t>() >= 0))
adaptiveRssTable[k] = v.get<uint64_t>();
} catch (...) { continue; }
}
}
} catch (...) { adaptiveRssTable.clear(); }
@@ -120,8 +126,11 @@ static void adaptiveLoadConfig()
auto j = nlohmann::json::parse(*m);
if (j.is_object())
for (auto & [k, v] : j.items())
if (v.is_string())
adaptiveMetricsMap[k] = v.get<std::string>();
// Per-entry guard: skip one bad value, keep the rest.
try {
if (v.is_string())
adaptiveMetricsMap[k] = v.get<std::string>();
} catch (...) { continue; }
}
} catch (...) { adaptiveMetricsMap.clear(); }
}
@@ -143,8 +152,12 @@ static std::optional<uint64_t> adaptiveEstPeakRSS(const StorePath & drvPath)
/* TCP-connect the metrics endpoint and read one line:
* "MemAvail_kB psi_mem psi_io psi_cpu load1 nproc"
* Short (500ms) connect+read timeout so selection NEVER hangs. Any failure
* returns an AdaptiveProbe with ok=false. */
* A single ~500ms wall-clock deadline bounds the WHOLE probe (resolve +
* connect + read) so selection NEVER hangs, regardless of a slow or
* byte-dribbling peer. The endpoint MUST be a numeric IP:port - resolution is
* pinned to AI_NUMERICHOST|AI_NUMERICSERV so getaddrinfo never does network
* I/O (a hostname simply fails fast -> fail-open). Any failure returns an
* AdaptiveProbe with ok=false. */
static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport)
{
AdaptiveProbe r;
@@ -154,10 +167,20 @@ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport)
std::string host = hostport.substr(0, colon);
std::string port = hostport.substr(colon + 1);
// Single wall-clock budget for the entire probe.
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
auto remainingMs = [&]() -> int {
auto d = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count();
return d <= 0 ? 0 : (int) d;
};
struct addrinfo hints;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
// Numeric-only: no DNS, no resolver blocking. Non-IP endpoint -> fail-open.
hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
struct addrinfo * res = nullptr;
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || !res)
return r;
@@ -170,7 +193,7 @@ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport)
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT;
if (poll(&pfd, 1, 500) <= 0) { close(fd); freeaddrinfo(res); return r; }
if (poll(&pfd, 1, remainingMs()) <= 0) { close(fd); freeaddrinfo(res); return r; }
int soerr = 0;
socklen_t sl = sizeof soerr;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl) < 0 || soerr != 0) {
@@ -181,16 +204,27 @@ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport)
}
freeaddrinfo(res);
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500000;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
/* Read one short line. Keep the socket non-blocking and gate every recv on
poll(POLLIN) against the shared deadline, so the total read time is
bounded even if the peer drips one byte at a time. A valid reply is tiny,
so also cap the number of reads. */
std::string line;
char buf[512];
while (line.size() < 4096) {
for (int iter = 0; iter < 16 && line.size() < 4096; ++iter) {
int rem = remainingMs();
if (rem == 0) break;
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN;
int pr = poll(&pfd, 1, rem);
if (pr <= 0) break; // timeout or error -> fail-open
if (!(pfd.revents & POLLIN)) break; // POLLHUP/POLLERR with no data
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n <= 0) break;
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
break;
}
if (n == 0) break; // peer closed
line.append(buf, n);
if (line.find('\n') != std::string::npos) break;
}