daemon: Phase 0 platform seams (ADR 0020) — eventfd/SO_PEERCRED/instance-lock/XDG behind platform/, zero behaviour change

Introduces the four rpc-layer seams docs/08-porting.md calls for this lane:
platform::Wakeup (eventfd), platform::peer_of (SO_PEERCRED/struct ucred, same-UID
check preserved unchanged), platform::acquire_instance_lock (abstract-namespace
socket), platform::runtime_base_dir/data_base_dir (XDG lookups). Today's Linux
code moves unchanged into daemon/src/rpc/platform/linux/; the seam headers
carry no OS types and no #ifdef.

No fifth interface for timerfd: EventLoop gains a portable add_timer() that
folds the next deadline into poll()'s own timeout, replacing both timerfd
instances in main.cpp — the loop already computes a deadline, so this needs no
per-OS backend at all.

Full suite 59/59 green; no #ifdef outside platform/linux/, no behaviour change.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-15 17:54:32 +04:00
co-authored by Claude Sonnet 5
parent 4e87730ce9
commit 6d7c4f7bc4
17 changed files with 321 additions and 143 deletions
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Single-instance guard, keyed by the resolved runtime directory (see rpc/runtime_dir.hpp)
// so isolated instances pointed at different runtime dirs never contend (docs/01 §2). The
// mechanism is Linux's abstract-namespace Unix socket; macOS has no abstract namespace and
// uses a real socket file plus flock() instead, which must unlink a stale socket left by a
// crashed process (docs/08-porting.md "API mapping" — the abstract version got that for
// free from the kernel).
#include <string>
#include <system_error>
namespace velox::daemon::rpc::platform {
// Returns the held fd (kept open for the process lifetime; closing it releases the lock)
// or -1 if another process already holds the lock for this exact `runtime_dir`, or on any
// other error acquiring it.
int acquire_instance_lock(const std::string& runtime_dir);
} // namespace velox::daemon::rpc::platform
@@ -0,0 +1,38 @@
#include "rpc/platform/instance_lock.hpp"
#include <cstddef>
#include <cstring>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include "util/crypto.hpp"
namespace velox::daemon::rpc::platform {
int acquire_instance_lock(const std::string& runtime_dir) {
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return -1;
// Truncated to 16 hex chars (64 bits): a collision would need two distinct runtime
// dirs to hash together, which is not a security boundary here — the socket itself is
// still 0600 same-UID-checked; this is only "don't let two daemons stomp each other".
const std::string name =
"velox-daemon-" + velox::daemon::crypto::sha256_hex(runtime_dir).substr(0, 16);
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
// Leading NUL selects the abstract namespace; the name follows, not NUL-terminated.
addr.sun_path[0] = '\0';
std::memcpy(addr.sun_path + 1, name.c_str(), name.size());
const socklen_t len =
static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + 1 + name.size());
if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), len) != 0) {
::close(fd);
return -1;
}
return fd;
}
} // namespace velox::daemon::rpc::platform
+17
View File
@@ -0,0 +1,17 @@
#include "rpc/platform/peer_id.hpp"
#include <sys/socket.h>
namespace velox::daemon::rpc::platform {
std::error_code peer_of(int fd, PeerId& out) {
ucred cred{};
socklen_t len = sizeof(cred);
if (::getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
return std::error_code(errno, std::generic_category());
}
out.uid = cred.uid;
return {};
}
} // namespace velox::daemon::rpc::platform
@@ -0,0 +1,44 @@
#include "rpc/platform/runtime_paths.hpp"
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
namespace velox::daemon::rpc::platform {
namespace {
std::error_code errc(int e) { return std::error_code(e, std::generic_category()); }
} // namespace
std::error_code runtime_base_dir(std::string& out) {
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') {
out = xdg;
return {};
}
const std::string base = "/run/user/" + std::to_string(::geteuid());
struct stat st{};
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
// No XDG_RUNTIME_DIR and no /run/user/<uid>: refuse rather than pick an insecure
// fallback. The caller surfaces this as "cannot start".
return errc(ENOENT);
}
out = base;
return {};
}
std::error_code data_base_dir(std::string& out) {
if (const char* xdg = ::getenv("XDG_DATA_HOME"); xdg != nullptr && xdg[0] != '\0') {
out = xdg;
return {};
}
if (const char* home = ::getenv("HOME"); home != nullptr && home[0] != '\0') {
out = std::string(home) + "/.local/share";
return {};
}
return errc(ENOENT);
}
} // namespace velox::daemon::rpc::platform
+32
View File
@@ -0,0 +1,32 @@
#include "rpc/platform/wakeup.hpp"
#include <sys/eventfd.h>
#include <unistd.h>
#include <cstdint>
#include <stdexcept>
namespace velox::daemon::rpc::platform {
Wakeup::Wakeup() {
fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (fd_ < 0) throw std::runtime_error("eventfd() failed");
}
Wakeup::~Wakeup() {
if (fd_ >= 0) ::close(fd_);
}
void Wakeup::signal() noexcept {
const std::uint64_t one = 1;
// Best-effort: an EAGAIN here means a wakeup is already pending, which is fine.
[[maybe_unused]] ssize_t n = ::write(fd_, &one, sizeof(one));
}
void Wakeup::drain() noexcept {
std::uint64_t sink = 0;
while (::read(fd_, &sink, sizeof(sink)) > 0) {
}
}
} // namespace velox::daemon::rpc::platform
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Identifies the process on the other end of a connected Unix-domain socket, for the
// same-UID check that is the Unix transport's authorization boundary (docs/01 §2,
// CLAUDE.md §4). `SO_PEERCRED`/`struct ucred` is Linux-only; macOS has `getpeereid`,
// Windows named pipes carry a token instead (docs/08-porting.md "The seams" /
// "API mapping"). The same-UID check itself is the security property and must not change
// per-OS (docs/adr/0020 decision 2).
#include <system_error>
namespace velox::daemon::rpc::platform {
struct PeerId {
unsigned int uid = 0;
};
// On success, fills `out` with the peer's identity of the already-connected `fd`. On
// failure, `out` is untouched and the error_code explains why (matches errno on Linux).
std::error_code peer_of(int fd, PeerId& out);
} // namespace velox::daemon::rpc::platform
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Where the OS wants ephemeral runtime state and persistent user data to live, before
// velox appends its own "/velox" subdirectory and applies the shared 0700-and-owned check
// (rpc/runtime_dir.cpp — that enforcement is portable POSIX logic and stays there; only
// "which base directory" is per-OS). Linux: XDG. macOS: $TMPDIR (runtime) and
// ~/Library/Application Support (data) — see docs/08-porting.md "API mapping".
#include <string>
#include <system_error>
namespace velox::daemon::rpc::platform {
// The base directory ephemeral runtime state (sockets, lock files) should live under,
// e.g. "$XDG_RUNTIME_DIR" or "/run/user/<uid>" on Linux. No trailing slash.
std::error_code runtime_base_dir(std::string& out);
// The base directory persistent user data should live under, e.g. "$XDG_DATA_HOME" or
// "~/.local/share" on Linux. No trailing slash.
std::error_code data_base_dir(std::string& out);
} // namespace velox::daemon::rpc::platform
+36
View File
@@ -0,0 +1,36 @@
#pragma once
// The event-loop wakeup primitive (docs/adr/0020, docs/08-porting.md). EventLoop uses this
// to interrupt a blocked poll() from another thread or a signal handler — the eventfd
// mechanics themselves are Linux-only; every other OS backend just needs something
// poll()-able that signal()/drain() can drive the same way (docs/08 §"The seams": a
// self-pipe on macOS, an event object on Windows).
//
// One implementation file per OS under platform/<os>/wakeup.cpp; this header carries no
// OS types and no #ifdef (ADR 0020 decision 1).
namespace velox::daemon::rpc::platform {
class Wakeup {
public:
Wakeup();
~Wakeup();
Wakeup(const Wakeup&) = delete;
Wakeup& operator=(const Wakeup&) = delete;
// The fd to register with poll(2) for readability.
int pollfd() const noexcept { return fd_; }
// Make pollfd() readable. Async-signal-safe and thread-safe.
void signal() noexcept;
// Drain whatever signal() queued so pollfd() stops being readable. Call this from the
// loop thread once pollfd() fires.
void drain() noexcept;
private:
int fd_ = -1;
};
} // namespace velox::daemon::rpc::platform