Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<uid>/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
Expand Down
4 changes: 2 additions & 2 deletions cli/args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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]);
Expand Down
48 changes: 38 additions & 10 deletions lib/network_lease6.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (C) 2026 by Vladyslav V. Prodan <github.com/click0>. All rights reserved.

#include "network_lease6.h"
#include "privops_client.h"
#include "runtime_paths_pure.h"
#include "err.h"

#include <fcntl.h>
Expand All @@ -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/<uid>/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;
}
Expand Down Expand Up @@ -70,7 +95,7 @@ std::vector<Ip6AllocPure::Lease6> parseAll(const std::string &buf) {

void writeAllAtomic(int /*lockedFd*/,
const std::vector<Ip6AllocPure::Lease6> &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);
Expand All @@ -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<Ip6AllocPure::Lease6> 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);
Expand Down
5 changes: 4 additions & 1 deletion lib/network_lease6.h
Original file line number Diff line number Diff line change
Expand Up @@ -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/<uid>/network-leases6.txt
// Format: one "<name> <ip6>" per line; lines starting with `#` and
// empty lines are ignored. Atomic writes via tmpfile +
// rename + flock(2).
Expand Down
Loading