From 0950f93f5f582b1a15024645d1c8260f2c0b4681 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 15:35:48 +0000 Subject: [PATCH] =?UTF-8?q?0.9.27=20=E2=80=94=20rootless:=20per-user=20lea?= =?UTF-8?q?se=20file=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twenty-eighth 0.9.x release. IP-lease file (network-leases.txt) moves from single shared /var/run/crate/ to per-user /var/run/crate// subtree when crated's privops socket is detected. lib/network_lease.cpp::effectivePath() — lazily resolves the path on first use: - g_pathOverridden (setPathForTesting): honour override - privops socket detected: per-user /var/run/crate//network-leases.txt - else: legacy /var/run/crate/network-leases.txt All 7 call sites (openLocked, readAll, writeAllAtomic, leasePath) replaced g_path with effectivePath(). Same path cached for process lifetime. Combined with 0.9.10 sub-CIDR allocator, alice's crate run never reads or writes bob's leases. Two operators can both run a jail named "web" simultaneously without IP collision. Trade-offs: - Path locked in at process start (cached after first call). Acceptable for short-lived crate run. - No auto-migration of existing legacy leases — operator runs crate clean + crate run to rebuild per-user. Suite: 1301 (unchanged — existing tests use setPathForTesting override path). Remaining: RCTL umbrella (0.9.28), default flip (0.9.29), setuid removed (1.0.0). --- CHANGELOG.md | 107 ++++++++++++++++++++++++++++++++++++++++++ cli/args.cpp | 4 +- lib/network_lease.cpp | 58 +++++++++++++++++++---- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e50bb..4a6aa1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,113 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [0.9.27] — 2026-05-09 + +**Rootless track, per-user lease file path.** Twenty-eighth +0.9.x release. The IP-lease file (`network-leases.txt`) +moves from a single shared `/var/run/crate/` location to a +per-user `/var/run/crate//` subtree when crated's +privops socket is detected. + +### What lands + +`lib/network_lease.cpp::effectivePath()` — lazily resolves +the lease file path on first use: + +```cpp +const std::string &effectivePath() { + static std::string cached; + static bool computed = false; + if (!computed) { + if (g_pathOverridden) { + cached = g_path; // honour test override + } else if (!PrivOpsClient::detectSocketPath().empty()) { + cached = RuntimePathsPure::perUserRoot((uint32_t)::getuid()) + + "/network-leases.txt"; + } else { + cached = g_path; // legacy single-tenant + } + computed = true; + } + return cached; +} +``` + +All call sites (`openLocked`, `readAll`, `writeAllAtomic`, +`leasePath()`) replaced `g_path.c_str()` references with +`effectivePath().c_str()`. Same path for the entire process +lifetime; cache eliminates per-call detection overhead. + +### Behavior + +| Mode | Lease file path | +|------|-----------------| +| Legacy (no privops socket) | `/var/run/crate/network-leases.txt` | +| Rootless (socket detected) | `/var/run/crate//network-leases.txt` | +| Test override (`setPathForTesting`) | the supplied path | + +The 0.9.10 sub-CIDR allocator already gives each operator a +disjoint IP range. Combined with this per-user lease file, +**alice's `crate run` never reads or writes bob's leases** +— two operators can both run a jail named `web` simultaneously +without IP collision. + +### Trade-offs + +- **Lease file location lock-in at process start.** The path + is computed once on first call to `effectivePath()` and + cached for the lifetime of the process. If the operator + starts/stops crated mid-`crate run`, the lease file + remains where it was first resolved. Acceptable: `crate + run` invocations are short-lived. +- **Migration of existing lease entries.** Operators with + pre-0.9.27 deployments switching to rootless mode will + see an empty per-user lease file at first; existing leases + in the legacy path are not auto-imported. Documented in + the migration doc; manual `crate clean` + `crate run` + rebuilds them per-user. + +### Series state + +CLI call-sites wired: +- `crate retune` (0.9.15) +- `crate stop` (0.9.17) +- `crate run` ZFS attach + detach (0.9.18) +- `crate run` nullfs mounts 8 sites (0.9.19) +- `crate run` vnet moveToVnet 4 sites (0.9.20) +- `crate run` removeJail teardown (0.9.21) +- `crate run` createJail (0.9.22) +- `crate run` setUp + disableOffload 5 sites (0.9.23) +- `crate run` bridge add + del 2 sites (0.9.24) +- `crate run` setInetAddr (0.9.25) +- `crate run` createEpair 2 sites (0.9.26) +- **`crate run` lease file path → per-user under + /var/run/crate// ← this release** + +Remaining: +- 0.9.28 — RCTL umbrella application (uses 0.9.11 + loginclass to apply `loginclass:crate-:KEY:deny=...` + rules at jail-create time) +- 0.9.29 — default flip (`rootless_per_user: true` + becomes default in `crated.conf.sample`) +- 1.0.0 — setuid bit removed from `Makefile install` + +### Tests + +No new tests added — existing `network_lease`-using tests +continue to pass via the `g_pathOverridden` test-override +path (set by `setPathForTesting`). Suite stays at 1301. + +### Files + +- `lib/network_lease.cpp` — `#include "privops_client.h"` + + `#include "runtime_paths_pure.h"` + `effectivePath()` + helper + `g_pathOverridden` flag + 7 call-site updates +- `cli/args.cpp` — version `crate 0.9.27` +- `CHANGELOG.md` — entry + +--- + ## [0.9.26] — 2026-05-09 **Rootless track, `create_epair` verb — first response-data diff --git a/cli/args.cpp b/cli/args.cpp index 157446e..942d471 100644 --- a/cli/args.cpp +++ b/cli/args.cpp @@ -753,7 +753,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.noColor = true; break; } else if (strEq(argv[a], "--version")) { - std::cout << "crate 0.9.26" << std::endl; + std::cout << "crate 0.9.27" << std::endl; exit(0); } else if (auto argShort = isShort(argv[a])) { switch (argShort) { @@ -764,7 +764,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.logProgress = true; break; case 'V': - std::cout << "crate 0.9.26" << std::endl; + std::cout << "crate 0.9.27" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/lib/network_lease.cpp b/lib/network_lease.cpp index 36cd0ac..154d8eb 100644 --- a/lib/network_lease.cpp +++ b/lib/network_lease.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2026 by Vladyslav V. Prodan . All rights reserved. #include "network_lease.h" +#include "privops_client.h" +#include "runtime_paths_pure.h" #include "err.h" #include @@ -20,20 +22,47 @@ namespace NetworkLease { namespace { +// 0.9.27: legacy single-tenant default. The actual path used at +// runtime comes from `effectivePath()` below — when crated's +// privops socket is detected, the lease file moves under +// /var/run/crate//network-leases.txt so each operator's +// IP allocations stay isolated. Tests + the explicit +// setPathForTesting() override route still use g_path directly. std::string g_path = "/var/run/crate/network-leases.txt"; +bool g_pathOverridden = false; // set by setPathForTesting + +// Resolve the lease file path. Lazily caches the per-uid path +// when rootless mode is active. Same path for the entire +// process lifetime. +const std::string &effectivePath() { + static std::string cached; + static bool computed = false; + if (!computed) { + if (g_pathOverridden) { + cached = g_path; // honour test override + } else if (!PrivOpsClient::detectSocketPath().empty()) { + cached = RuntimePathsPure::perUserRoot((uint32_t)::getuid()) + + "/network-leases.txt"; + } else { + cached = g_path; // legacy single-tenant + } + computed = true; + } + return cached; +} // Open (creating) the lease file and acquire an exclusive lock. // Returns the fd. Caller must ::close() it. int openLocked() { - int fd = ::open(g_path.c_str(), + int fd = ::open(effectivePath().c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0640); if (fd < 0) - ERR("cannot open " << g_path << ": " << std::strerror(errno)) + ERR("cannot open " << effectivePath() << ": " << std::strerror(errno)) if (::flock(fd, LOCK_EX) != 0) { int e = errno; ::close(fd); - ERR("flock " << g_path << ": " << std::strerror(e)) + ERR("flock " << effectivePath() << ": " << std::strerror(e)) } return fd; } @@ -78,7 +107,7 @@ std::vector parseAll(const std::string &buf) { // crash mid-write doesn't leave a corrupt half-written file. void writeAllAtomic(int /*lockedFd*/, const std::vector &leases) { - std::string tmp = g_path + ".tmp"; + std::string tmp = effectivePath() + ".tmp"; int fd = ::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0640); @@ -102,25 +131,34 @@ void writeAllAtomic(int /*lockedFd*/, } ::fsync(fd); ::close(fd); - if (::rename(tmp.c_str(), g_path.c_str()) != 0) { + if (::rename(tmp.c_str(), effectivePath().c_str()) != 0) { int e = errno; ::unlink(tmp.c_str()); - ERR("rename " << tmp << " -> " << g_path << ": " << std::strerror(e)) + ERR("rename " << tmp << " -> " << effectivePath() << ": " << std::strerror(e)) } } } // anon -const std::string &leasePath() { return g_path; } -void setPathForTesting(const std::string &path) { g_path = path; } +const std::string &leasePath() { return effectivePath(); } +void setPathForTesting(const std::string &path) { + g_path = path; + g_pathOverridden = true; + // Note: effectivePath()'s static cache is computed on first + // call, so for tests that exercise setPathForTesting after + // effectivePath() has already cached, the override won't take + // effect. Tests that need to swap paths mid-run should fork + // (ATF tests already run in subprocesses), giving each test + // its own first-call cache. +} std::vector readAll() { // Open without lock for read-only callers (e.g. `crate doctor`, // future). Tolerate ENOENT. - int fd = ::open(g_path.c_str(), O_RDONLY | O_CLOEXEC); + int fd = ::open(effectivePath().c_str(), O_RDONLY | O_CLOEXEC); if (fd < 0) { if (errno == ENOENT) return {}; - ERR("open " << g_path << ": " << std::strerror(errno)) + ERR("open " << effectivePath() << ": " << std::strerror(errno)) } auto buf = readAllFd(fd); ::close(fd);