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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [1.1.20] — 2026-07-05

**Fix: IPv6 lease parser didn't validate the jail-name charset.**
`Ip6AllocPure::parseLeaseLine6` only rejected an empty name, while its
IPv4 twin (`IpAllocPure::parseLeaseLine` → `nameLooksValid`) restricts
the name to `[A-Za-z0-9._-]`, 1–64 chars. A hand-edited or corrupt
IPv6 lease line whose name held control characters, embedded
whitespace, or `..` / `/` path-traversal parsed as **valid** on the v6
path and flowed downstream — an asymmetry with the v4 path that already
rejected them.

`parseLeaseLine6` now applies the same charset check (a local
`leaseNameLooksValid` mirroring the v4 rule) and rejects trailing
whitespace in the address, closing the v4/v6 gap. Found in the
project-wide pure-module audit.

New `ip6_alloc_pure_test` case rejects traversal / slash / shell-meta /
tab / oversized names and confirms a clean name still round-trips.

## [1.1.19] — 2026-06-10

**Security: fail closed when `getpeereid` fails on the privops libnv
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.1.19" << std::endl;
std::cout << "crate 1.1.20" << 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.1.19" << std::endl;
std::cout << "crate 1.1.20" << std::endl;
exit(0);
default:
err("unsupported short option '%s'", argv[a]);
Expand Down
4 changes: 2 additions & 2 deletions docs/trust-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
operators on one machine) and contributors extending the privileged
surface.

**Applies to:** 1.1.19 (rootless model + per-tenant authz series 1.1.12 →
**Applies to:** 1.1.20 (rootless model + per-tenant authz series 1.1.12 →
1.1.17 covering every privops verb that carries an operator-controlled
ownership signal). For the ≤ 0.9.x setuid model and the migration, see
[`rootless-migration.md`](rootless-migration.md).
Expand Down Expand Up @@ -62,7 +62,7 @@ surface; 1.0.0 removed the setuid bit (`Makefile`, comment at the
operator and delegates privileged operations to crated(8)"*).

The single-trust-domain property did **not** disappear — it relocated.
Reasoning about isolation on 1.1.19 means reasoning about who can reach
Reasoning about isolation on 1.1.20 means reasoning about who can reach
**privops**, not who can run `crate(1)`.

---
Expand Down
4 changes: 2 additions & 2 deletions docs/trust-model.uk.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
(кілька операторів на одній машині), і контрибʼютори, які розширюють
привілейовану поверхню.

**Стосується:** 1.1.19 (rootless-модель + серія per-tenant authz 1.1.12 →
**Стосується:** 1.1.20 (rootless-модель + серія per-tenant authz 1.1.12 →
1.1.17 покриває кожен privops-верб з operator-controlled ownership-
сигналом). Про ≤ 0.9.x setuid-модель і міграцію див.
[`rootless-migration.md`](rootless-migration.md).
Expand Down Expand Up @@ -62,7 +62,7 @@ privops-сокета, ніколи admin-токен і ніколи Unix-сок
privileged operations to crated(8)»*).

Властивість «єдиний домен довіри» **не зникла** — вона переїхала.
Міркувати про ізоляцію на 1.1.19 — це міркувати про те, хто має доступ
Міркувати про ізоляцію на 1.1.20 — це міркувати про те, хто має доступ
до **privops**, а не хто може запустити `crate(1)`.

---
Expand Down
24 changes: 23 additions & 1 deletion lib/ip6_alloc_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ namespace Ip6AllocPure {

namespace {

// 1.1.20: mirror of IpAllocPure's v4 lease-name check (alnum + . _ -,
// 1..64 chars). Duplicated here rather than exported so the two lease
// modules stay decoupled; the rule must stay in sync with the v4 twin.
bool leaseNameLooksValid(const std::string &n) {
if (n.empty() || n.size() > 64) return false;
for (char c : n) {
bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c == '.' || c == '_' || c == '-';
if (!ok) return false;
}
return true;
}

bool isHexChar(char c) {
return (c >= '0' && c <= '9')
|| (c >= 'a' && c <= 'f')
Expand Down Expand Up @@ -249,7 +263,15 @@ std::string parseLeaseLine6(const std::string &line, Lease6 &out) {
if (sp == std::string::npos) return "no space separator";
auto name = line.substr(0, sp);
auto ip = line.substr(sp + 1);
if (name.empty()) return "empty name";
// 1.1.20: validate the jail name charset, matching the v4 twin
// (IpAllocPure::parseLeaseLine -> nameLooksValid). Before this, a
// hand-edited or corrupt IPv6 lease line whose name held control
// chars, embedded whitespace, or `..`/`/` path-traversal parsed as
// VALID and flowed downstream (the v4 path already rejected these).
if (ip.find_first_of(" \t\r\n") != std::string::npos)
return "ip has internal whitespace";
if (!leaseNameLooksValid(name))
return "invalid jail name '" + name + "'";
Addr6 a{};
if (auto e = parseIp6(ip, a); !e.empty())
return "ip parse: " + e;
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/ip6_alloc_pure_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,22 @@ ATF_TEST_CASE_BODY(lease_parse_rejects_garbage) {
ATF_REQUIRE(!parseLeaseLine6("name not-an-ip", out).empty());
}

ATF_TEST_CASE_WITHOUT_HEAD(lease_parse_rejects_bad_name);
ATF_TEST_CASE_BODY(lease_parse_rejects_bad_name) {
// 1.1.20: the jail-name charset is now validated (matching the v4
// twin). Names with path-traversal, control chars, or shell-ish
// punctuation must be rejected even though the IP is well-formed.
Lease6 out;
ATF_REQUIRE(!parseLeaseLine6("../../etc fd00::5", out).empty()); // traversal
ATF_REQUIRE(!parseLeaseLine6("a/b fd00::5", out).empty()); // slash
ATF_REQUIRE(!parseLeaseLine6("na$me fd00::5", out).empty()); // shell meta
ATF_REQUIRE(!parseLeaseLine6(std::string("na\tme fd00::5"), out).empty()); // tab in name
ATF_REQUIRE(!parseLeaseLine6(std::string(65, 'x') + " fd00::5", out).empty()); // >64
// A clean name with a well-formed address still round-trips.
ATF_REQUIRE_EQ(parseLeaseLine6("web-1.app_2 fd00::5", out), std::string());
ATF_REQUIRE_EQ(out.name, std::string("web-1.app_2"));
}

ATF_INIT_TEST_CASES(tcs) {
ATF_ADD_TEST_CASE(tcs, parseIp6_full_form);
ATF_ADD_TEST_CASE(tcs, parseIp6_shorthand_middle);
Expand All @@ -239,4 +255,5 @@ ATF_INIT_TEST_CASES(tcs) {
ATF_ADD_TEST_CASE(tcs, inPool_predicate);
ATF_ADD_TEST_CASE(tcs, lease_round_trip);
ATF_ADD_TEST_CASE(tcs, lease_parse_rejects_garbage);
ATF_ADD_TEST_CASE(tcs, lease_parse_rejects_bad_name);
}
Loading