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
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<uid>/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 <name>` now asks `SpecRegistry::lookup(name)`
for the `.crate` path before falling back to the legacy
filesystem walk under `/var/run/crate/<name>.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
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.1" << std::endl;
std::cout << "crate 1.0.2" << 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.1" << std::endl;
std::cout << "crate 1.0.2" << std::endl;
exit(0);
default:
err("unsupported short option '%s'", argv[a]);
Expand Down
24 changes: 17 additions & 7 deletions lib/lifecycle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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/<name>.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/<name>.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)) {
Expand Down
48 changes: 38 additions & 10 deletions lib/spec_registry.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 "spec_registry.h"
#include "privops_client.h"
#include "runtime_paths_pure.h"
#include "err.h"

#include <fcntl.h>
Expand All @@ -20,18 +22,41 @@ namespace SpecRegistry {

namespace {

// 1.0.2: legacy single-tenant default. `effectivePath()` lazy-
// resolves to /var/run/crate/<uid>/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;
}
Expand Down Expand Up @@ -70,7 +95,7 @@ std::vector<SpecRegistryPure::Entry> parseAll(const std::string &buf) {

void writeAllAtomic(int /*lockedFd*/,
const std::vector<SpecRegistryPure::Entry> &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);
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 &registryPath() { return g_path; }
void setPathForTesting(const std::string &p) { g_path = p; }
const std::string &registryPath() { return effectivePath(); }
void setPathForTesting(const std::string &p) {
g_path = p;
g_pathOverridden = true;
}

std::vector<SpecRegistryPure::Entry> 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: 5 additions & 0 deletions lib/spec_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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/<uid>/spec-registry.txt
//

#include "spec_registry_pure.h"

Expand Down
Loading