diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2e0ed..0d317de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,85 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [1.0.2] — 2026-05-12 + +**Spec registry per-user + restart wires through it.** Second +patch release of the 1.x line. Fixes two coupled multi-tenant +bugs that the 0.8.21 spec-registry feature inherited from the +pre-rootless filesystem-walk era. + +### What changes + +#### `lib/spec_registry.cpp` — lazy per-user path + +Same `effectivePath()` lazy-resolve pattern as +`network_lease.cpp` (0.9.27) and `network_lease6.cpp` (1.0.1). +When the privops socket is detected, the registry file moves +from `/var/run/crate/spec-registry.txt` to +`/var/run/crate//spec-registry.txt`. Alice's +`crate run -f web.crate` registers under alice's uid; bob +doing the same registers under bob's uid; neither sees the +other's entry. + +#### `lib/lifecycle.cpp` — `restartCrate()` queries the registry + +`crate restart ` now asks `SpecRegistry::lookup(name)` +for the `.crate` path before falling back to the legacy +filesystem walk under `/var/run/crate/.crate`. The +fallback is preserved for two reasons: + +1. Jails started before 0.8.21 (no registry entry, just a + conventional file placement) +2. Single-tenant deployments that historically dropped + `.crate` files manually under `/var/run/crate/` + +Net effect: existing single-tenant homelabs see no behaviour +change; rootless multi-tenant deployments stop cross- +contaminating. + +### Why this matters + +Before this release, two operators on the same host running +`crate restart web` would race to find each other's `.crate` +path. Whoever pushed last to the shared +`/var/run/crate/spec-registry.txt` won. Cross-tenant +restarts then either picked up the wrong spec (silent data +corruption) or hit the legacy filesystem walk (silent +fall-through to a stale `/var/run/crate/web.crate` left over +from a previous deploy). + +### 1.x backlog + +Remaining latent per-user path leaks from the pre-1.0.0 audit: + +- `lib/pfctl_ops.cpp` pf lock not per-user +- `lib/stack.cpp` DNS dirs hardcoded +- `lib/vm_run.cpp` VM + cloud-init paths hardcoded +- `lib/run_net.cpp:446` direct `ifconfig -vnet` (should use + existing `SetIfaceUp` privops verb) +- Query-side privops verbs (inspect/doctor/migrate shell out) + +### Tests + +Existing `spec_registry_pure_test` covers parser + line +format. The `effectivePath()` lazy-cache uses the same pattern +proven in `network_lease.cpp`. No new test files; suite stays +at 1303. + +### Files + +- `lib/spec_registry.cpp` — `effectivePath()` helper, all I/O + routed through it; `setPathForTesting` sets an override flag +- `lib/spec_registry.h` — header comment documents per-user + storage +- `lib/lifecycle.cpp` — `restartCrate` queries SpecRegistry + first, legacy fs walk as fallback; adds + `#include "spec_registry.h"` +- `cli/args.cpp` — version `crate 1.0.2` +- `CHANGELOG.md` — this entry + +--- + ## [1.0.1] — 2026-05-12 **IPv6 lease file per-user.** First patch release of the 1.x diff --git a/cli/args.cpp b/cli/args.cpp index d88bf51..ddb7e75 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 1.0.1" << std::endl; + std::cout << "crate 1.0.2" << 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 1.0.1" << std::endl; + std::cout << "crate 1.0.2" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/lib/lifecycle.cpp b/lib/lifecycle.cpp index 8752c14..215c8f8 100644 --- a/lib/lifecycle.cpp +++ b/lib/lifecycle.cpp @@ -9,6 +9,7 @@ #include "pathnames.h" #include "privops_client.h" #include "run_jail.h" +#include "spec_registry.h" #include "util.h" #include "err.h" @@ -456,13 +457,22 @@ bool restartCrate(const Args &args) { if (!stopCrate(stopArgs)) return false; - // Look for .crate file to restart from - // Convention: crate file is stored at /var/run/crate/.crate - auto crateFile = STR("/var/run/crate/" << jailName << ".crate"); - if (!Util::Fs::fileExists(crateFile)) { - // Try with path-based name - auto bareName = Util::filePathToBareName(jailPath); - crateFile = STR("/var/run/crate/" << bareName << ".crate"); + // 1.0.2: prefer the spec-registry mapping (populated by `crate run -f` + // since 0.8.21) over a filesystem walk. The registry is per-user when + // crated's privops socket is detected, so this also fixes the + // multi-tenant case where bob's `crate restart web` previously + // picked up alice's `.crate` path from the shared file. + std::string crateFile = SpecRegistry::lookup(jailName); + + // Legacy fallback for jails started before 0.8.21 (no registry + // entry) or single-tenant deployments that placed the spec under + // the conventional /var/run/crate/.crate path. + if (crateFile.empty() || !Util::Fs::fileExists(crateFile)) { + crateFile = STR("/var/run/crate/" << jailName << ".crate"); + if (!Util::Fs::fileExists(crateFile)) { + auto bareName = Util::filePathToBareName(jailPath); + crateFile = STR("/var/run/crate/" << bareName << ".crate"); + } } if (!Util::Fs::fileExists(crateFile)) { diff --git a/lib/spec_registry.cpp b/lib/spec_registry.cpp index 9624da8..27ac8cf 100644 --- a/lib/spec_registry.cpp +++ b/lib/spec_registry.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2026 by Vladyslav V. Prodan . All rights reserved. #include "spec_registry.h" +#include "privops_client.h" +#include "runtime_paths_pure.h" #include "err.h" #include @@ -20,18 +22,41 @@ namespace SpecRegistry { namespace { +// 1.0.2: legacy single-tenant default. `effectivePath()` lazy- +// resolves to /var/run/crate//spec-registry.txt when the +// crated privops socket is detected, so two operators on the +// same host don't see each other's jail-name → .crate mapping. +// Mirrors NetworkLease (0.9.27) and NetworkLease6 (1.0.1). std::string g_path = "/var/run/crate/spec-registry.txt"; +bool g_pathOverridden = false; + +const std::string &effectivePath() { + static std::string cached; + static bool computed = false; + if (!computed) { + if (g_pathOverridden) { + cached = g_path; + } else if (!PrivOpsClient::detectSocketPath().empty()) { + cached = RuntimePathsPure::perUserRoot((uint32_t)::getuid()) + + "/spec-registry.txt"; + } else { + cached = g_path; + } + computed = true; + } + return cached; +} 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; } @@ -70,7 +95,7 @@ std::vector parseAll(const std::string &buf) { void writeAllAtomic(int /*lockedFd*/, const std::vector &entries) { - 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); @@ -94,23 +119,26 @@ 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 ®istryPath() { return g_path; } -void setPathForTesting(const std::string &p) { g_path = p; } +const std::string ®istryPath() { return effectivePath(); } +void setPathForTesting(const std::string &p) { + g_path = p; + g_pathOverridden = true; +} std::vector readAll() { - 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); diff --git a/lib/spec_registry.h b/lib/spec_registry.h index 55ed7b1..0509b12 100644 --- a/lib/spec_registry.h +++ b/lib/spec_registry.h @@ -7,6 +7,11 @@ // {jail name -> .crate path} store. See lib/spec_registry_pure.h // for the design rationale and file format. // +// Storage: legacy single-tenant default +// /var/run/crate/spec-registry.txt +// rootless per-user (1.0.2+, when privops socket detected) +// /var/run/crate//spec-registry.txt +// #include "spec_registry_pure.h"