From d05d20366d46a96f880316b1bf1c585396f0b428 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 15:16:56 +0000 Subject: [PATCH] =?UTF-8?q?1.0.1=20=E2=80=94=20IPv6=20lease=20file=20per-u?= =?UTF-8?q?ser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the 0.9.27 IPv4 lazy-resolve pattern into network_lease6.cpp. When the crated privops socket is detected, the v6 lease file resolves to /var/run/crate//network-leases6.txt instead of the legacy shared /var/run/crate/network-leases6.txt path. Without this fix, rootless multi-tenant deployments raced on v6 allocations even though v4 was already isolated since 0.9.27 — an asymmetry that masked the bug from single-stack v4 setups. Wire/format/signatures unchanged. Suite stays at 1303. --- CHANGELOG.md | 73 ++++++++++++++++++++++++++++++++++++++++++ cli/args.cpp | 4 +-- lib/network_lease6.cpp | 48 +++++++++++++++++++++------ lib/network_lease6.h | 5 ++- 4 files changed, 117 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b3de4..6e2e0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,79 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [1.0.1] — 2026-05-12 + +**IPv6 lease file per-user.** First patch release of the 1.x +line. Mirrors the 0.9.27 IPv4 lazy-resolve into the IPv6 +sibling that was missed at the time. + +### What changes + +`lib/network_lease6.cpp` gains the same `effectivePath()` +helper that `network_lease.cpp` has had since 0.9.27. When the +crated privops socket is detected at first call, the IPv6 +lease file path resolves to: + +``` +/var/run/crate//network-leases6.txt +``` + +instead of the legacy single-tenant +`/var/run/crate/network-leases6.txt`. Empty-socket-path +deployments (no `crated` running, or unset +`privops_socket:`) preserve the legacy path byte-for-byte. + +### Why this matters + +Without this fix, every operator on a rootless host shared the +same v6 lease file, racing each other for the lock and seeing +each other's allocations. The v4 sibling was already isolated +since 0.9.27 — meaning two operators running parallel +`crate run` would race on v6 but not on v4, an +asymmetry that masked the bug from single-stack v4 +deployments. + +### Wire / API compatibility + +None of the lease format, allocation algorithm, or public +function signatures changed. `NetworkLease6::leasePath()` now +returns the resolved per-user path instead of the legacy +constant; callers that printed this value see the new path +when rootless mode is active. + +### 1.x backlog + +Remaining latent per-user path leaks from the pre-1.0.0 audit +(unchanged from 1.0.0): + +- `lib/lifecycle.cpp` `.crate` file path +- `lib/pfctl_ops.cpp` pf lock +- `lib/stack.cpp` DNS dirs +- `lib/vm_run.cpp` VM + cloud-init paths +- `lib/run_net.cpp:446` direct `ifconfig -vnet` (should use + `SetIfaceUp` privops verb) + +These will land in 1.0.2+ as mechanical mini-patches following +this PR's template. + +### Tests + +No new tests — the change mirrors the 0.9.27 pattern, which +is exercised by `tests/unit/ip6_alloc_pure_test.cpp` plus the +runtime-paths suite. The lazy-cache + override flag pair was +proven in network_lease.cpp. + +### Files + +- `lib/network_lease6.cpp` — `effectivePath()` helper, all + I/O routed through it +- `lib/network_lease6.h` — header comment updated to document + per-user storage path +- `cli/args.cpp` — version `crate 1.0.1` +- `CHANGELOG.md` — this entry + +--- + ## [1.0.0] — 2026-05-12 **Rootless track complete — setuid bit removed.** First 1.x diff --git a/cli/args.cpp b/cli/args.cpp index fc9486d..d88bf51 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.0" << std::endl; + std::cout << "crate 1.0.1" << 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.0" << std::endl; + std::cout << "crate 1.0.1" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/lib/network_lease6.cpp b/lib/network_lease6.cpp index 2bfb07e..6b918b4 100644 --- a/lib/network_lease6.cpp +++ b/lib/network_lease6.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2026 by Vladyslav V. Prodan . All rights reserved. #include "network_lease6.h" +#include "privops_client.h" +#include "runtime_paths_pure.h" #include "err.h" #include @@ -20,18 +22,41 @@ namespace NetworkLease6 { namespace { +// 1.0.1: 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-leases6.txt. Mirrors the IPv4 +// helper in network_lease.cpp (0.9.27). std::string g_path = "/var/run/crate/network-leases6.txt"; +bool g_pathOverridden = false; // set by setPathForTesting + +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()) + + "/network-leases6.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 &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); @@ -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 &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; +} 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/network_lease6.h b/lib/network_lease6.h index 2db30e3..cabf1df 100644 --- a/lib/network_lease6.h +++ b/lib/network_lease6.h @@ -7,7 +7,10 @@ // path. See lib/network_lease.h for the design rationale; the v6 // version is a near-copy with Ip6AllocPure types substituted. // -// Storage: /var/run/crate/network-leases6.txt +// Storage: legacy single-tenant default +// /var/run/crate/network-leases6.txt +// rootless per-user (1.0.1+, when privops socket detected) +// /var/run/crate//network-leases6.txt // Format: one " " per line; lines starting with `#` and // empty lines are ignored. Atomic writes via tmpfile + // rename + flock(2).