From 7674fdbd36cbf76c3907d91271e77be4e2d6a0be Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 18:44:09 +0000 Subject: [PATCH] =?UTF-8?q?0.9.28=20=E2=80=94=20rootless:=20set/clear=5Flo?= =?UTF-8?q?ginclass=5Frctl=20verbs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twenty-ninth 0.9.x release. Two new verbs that apply RCTL rules at the loginclass scope (umbrella) instead of per-jail. The 0.9.11 crate- loginclass infrastructure is now addressable end-to-end. set_loginclass_rctl wraps: rctl -a loginclass:::deny= clear_loginclass_rctl wraps: rctl -r loginclass:::deny Validators: PerUserRctlPure::validateLoginclassName (must be crate- shape) + RetunePure key/value whitelist (same gate as set_rctl). Use case: alice spawning 3 jails of 2G each = 6G total previously slipped per-jail caps. Umbrella enforces aggregate. Per-jail + umbrella apply simultaneously (kernel takes more restrictive). CLI wiring intentionally NONE for 0.9.28 — primitive only. Operator scripts call directly or wait for 0.9.29 which auto-applies from crated.conf at jail-create time (needs config-schema design). Wire-up: same pattern as 0.9.23-0.9.27 verb expansion. 2 new ATF tests + verb_token_roundtrips updated. Suite: 1301 -> 1303. --- CHANGELOG.md | 94 ++++++++++++++++++++++++++++++++ cli/args.cpp | 4 +- daemon/privops_handlers.cpp | 62 +++++++++++++++++++++ daemon/privops_handlers.h | 9 +++ lib/privops_client.h | 7 +++ lib/privops_client_pure.cpp | 20 +++++++ lib/privops_nv_pure.cpp | 15 +++++ lib/privops_nv_pure.h | 4 ++ lib/privops_pure.cpp | 20 +++++++ lib/privops_pure.h | 27 +++++++++ lib/privops_wire_pure.cpp | 41 ++++++++++++++ lib/privops_wire_pure.h | 13 +++++ tests/unit/privops_pure_test.cpp | 38 +++++++++++++ 13 files changed, 352 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a6aa1c..3af67c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,100 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [0.9.28] — 2026-05-09 + +**Rootless track, RCTL umbrella verbs.** Twenty-ninth 0.9.x +release. Two new verbs that apply RCTL rules at the +loginclass scope (umbrella) instead of per-jail. The +infrastructure for 0.9.11's `crate-` loginclass is now +addressable end-to-end. + +### What lands + +#### Two new privops verbs + +- **`set_loginclass_rctl`** — wraps + `rctl -a loginclass:::deny=`. Fields: + `loginclass`, `key`, `value`. Validates + `loginclass` via `PerUserRctlPure::validateLoginclassName` + (must be `crate-` shape), and `key`/`value` via + the existing `RetunePure` whitelist (same gate as + `set_rctl` from 0.9.0). +- **`clear_loginclass_rctl`** — symmetric remove via + `rctl -r loginclass:::deny`. Fields: + `loginclass`, `key`. + +#### Use case + +Today's `set_rctl` (0.9.0) is jail-scoped — alice can +exceed her aggregate quota by spawning multiple jails, +each below the per-jail cap. With `set_loginclass_rctl`, +the kernel enforces a sum across all of alice's jails: + +``` +# Pre-0.9.28: alice spawns 3 jails, each at 2G memoryuse. +# Total = 6G. Per-jail set_rctl can't catch this. + +# 0.9.28: at provisioning time, set the umbrella once: +POST /api/v1/privops/set_loginclass_rctl +{"loginclass":"crate-1000","key":"memoryuse","value":"4G"} + +# Now: alice spawns 3 jails. Kernel enforces 4G total +# regardless of how many jails she has. +``` + +The umbrella rule and per-jail rules apply simultaneously +— kernel takes the more restrictive of the two whenever +both fire. + +### Wire-up + +Same files as previous verb-expansion releases — +`privops_pure.{h,cpp}`, `privops_wire_pure.{h,cpp}`, +`privops_nv_pure.{h,cpp}`, `privops_client.h`, +`privops_client_pure.cpp`, `privops_handlers.{h,cpp}`. Two +new functions/cases per file. + +`privops_pure.cpp` gains `#include "per_user_rctl_pure.h"` +to reuse `validateLoginclassName`. + +### CLI wiring (intentionally none for 0.9.28) + +The verbs are primitives — no automatic invocation from +`crate run` or other CLI commands. Operators wanting +umbrella rules call them directly (e.g., from a startup +script) or wait for **0.9.29** which will auto-apply +umbrella rules from `crated.conf` at jail-create time. + +This split keeps 0.9.28 small and reviewable — pure verb +addition. Auto-application requires a config-schema decision +(per-key map vs structured spec) that's worth its own PR. + +### Series state + +CLI call-sites wired (12+ in total). All host-side verbs +needed for `crate run` exist. RCTL umbrella primitives now +exist; auto-application coming in 0.9.29. + +### Tests + +- 2 new ATF tests in `privops_pure_test` + (`set_loginclass_rctl_validates`, + `clear_loginclass_rctl_validates`) covering happy path + + bad loginclass + bad key + out-of-range value. +- `verb_token_roundtrips_for_every_verb` updated. +- Suite: 1301 → **1303**. + +### Files + +`privops_pure.{h,cpp}`, `privops_wire_pure.{h,cpp}`, +`privops_nv_pure.{h,cpp}`, `privops_client.h`, +`privops_client_pure.cpp`, `privops_handlers.{h,cpp}`, +`tests/unit/privops_pure_test.cpp`, `cli/args.cpp`, +`CHANGELOG.md`. + +--- + ## [0.9.27] — 2026-05-09 **Rootless track, per-user lease file path.** Twenty-eighth diff --git a/cli/args.cpp b/cli/args.cpp index 942d471..f18e1c0 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 0.9.27" << std::endl; + std::cout << "crate 0.9.28" << 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 0.9.27" << std::endl; + std::cout << "crate 0.9.28" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/daemon/privops_handlers.cpp b/daemon/privops_handlers.cpp index 29d1471..0cd303b 100644 --- a/daemon/privops_handlers.cpp +++ b/daemon/privops_handlers.cpp @@ -443,6 +443,36 @@ DispatchResult handleCreateEpair(const PrivOpsPure::CreateEpairReq &/*r*/) { pair.second)}; } +// --- handleSetLoginclassRctl / handleClearLoginclassRctl (0.9.28) --- + +DispatchResult handleSetLoginclassRctl(const PrivOpsPure::SetLoginclassRctlReq &r) { + // Build `rctl -a loginclass:::deny=` argv. + // Mirrors handleSetRctl (0.9.2) but with loginclass subject. + std::string rule = "loginclass:" + r.loginclass + ":" + r.key + + ":deny=" + r.rawValue; + try { + Util::execCommand({CRATE_PATH_RCTL, "-a", rule}, + "privops set_loginclass_rctl"); + } catch (const std::exception &e) { + return {500, PrivOpsWirePure::formatHandlerError("exec_failed", e.what())}; + } + return {200, PrivOpsWirePure::formatSetLoginclassRctlSuccess( + r.loginclass, r.key, r.rawValue)}; +} + +DispatchResult handleClearLoginclassRctl(const PrivOpsPure::ClearLoginclassRctlReq &r) { + // `rctl -r loginclass:::deny` — symmetric remove. + std::string subject = "loginclass:" + r.loginclass + ":" + r.key + ":deny"; + try { + Util::execCommand({CRATE_PATH_RCTL, "-r", subject}, + "privops clear_loginclass_rctl"); + } catch (const std::exception &e) { + return {500, PrivOpsWirePure::formatHandlerError("exec_failed", e.what())}; + } + return {200, PrivOpsWirePure::formatClearLoginclassRctlSuccess( + r.loginclass, r.key)}; +} + // --- Top-level dispatcher --- namespace { @@ -627,6 +657,22 @@ DispatchResult dispatchPrivOp(Verb v, const std::string &body, return {400, PrivOpsWirePure::formatValidateError(e)}; return handleCreateEpair(r); } + case Verb::SetLoginclassRctl: { + PrivOpsPure::SetLoginclassRctlReq r; + if (auto e = PrivOpsWirePure::parseSetLoginclassRctl(body, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateSetLoginclassRctl(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleSetLoginclassRctl(r); + } + case Verb::ClearLoginclassRctl: { + PrivOpsPure::ClearLoginclassRctlReq r; + if (auto e = PrivOpsWirePure::parseClearLoginclassRctl(body, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateClearLoginclassRctl(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleClearLoginclassRctl(r); + } default: return PrivOpsWirePure::parseValidateAndDispatch(v, body); } @@ -805,6 +851,22 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m, return {400, PrivOpsWirePure::formatValidateError(e)}; return handleCreateEpair(r); } + case Verb::SetLoginclassRctl: { + PrivOpsPure::SetLoginclassRctlReq r; + if (auto e = PrivOpsNvPure::parseSetLoginclassRctl(m, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateSetLoginclassRctl(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleSetLoginclassRctl(r); + } + case Verb::ClearLoginclassRctl: { + PrivOpsPure::ClearLoginclassRctlReq r; + if (auto e = PrivOpsNvPure::parseClearLoginclassRctl(m, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateClearLoginclassRctl(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleClearLoginclassRctl(r); + } case Verb::Unknown: return {404, std::string("{\"error\":\"unknown or missing 'verb' field\"}")}; diff --git a/daemon/privops_handlers.h b/daemon/privops_handlers.h index 7445d23..6dc100c 100644 --- a/daemon/privops_handlers.h +++ b/daemon/privops_handlers.h @@ -166,4 +166,13 @@ PrivOpsWirePure::DispatchResult handleSetIfaceInetAddr(const PrivOpsPure::SetIfa // the response body carries the assigned A/B iface names. PrivOpsWirePure::DispatchResult handleCreateEpair(const PrivOpsPure::CreateEpairReq &r); +// 0.9.28: per-loginclass RCTL umbrella ops. Wraps +// `rctl -a loginclass:::deny=` and +// `rctl -r loginclass:::deny`. The set_rctl +// (0.9.0) verb is jail-scoped; this is the umbrella variant +// that aggregates resource use across all of one operator's +// jails. +PrivOpsWirePure::DispatchResult handleSetLoginclassRctl(const PrivOpsPure::SetLoginclassRctlReq &r); +PrivOpsWirePure::DispatchResult handleClearLoginclassRctl(const PrivOpsPure::ClearLoginclassRctlReq &r); + } // namespace Crated diff --git a/lib/privops_client.h b/lib/privops_client.h index 1d3bebf..cb3acd1 100644 --- a/lib/privops_client.h +++ b/lib/privops_client.h @@ -129,6 +129,13 @@ PrivOpsNvPure::FieldMap buildSetIfaceInetAddr(const std::string &ifname, // them via PrivOpsWirePure::extractStringField. PrivOpsNvPure::FieldMap buildCreateEpair(); +// 0.9.28: per-loginclass RCTL umbrella ops. +PrivOpsNvPure::FieldMap buildSetLoginclassRctl(const std::string &loginclass, + const std::string &key, + const std::string &rawValue); +PrivOpsNvPure::FieldMap buildClearLoginclassRctl(const std::string &loginclass, + const std::string &key); + // --- Wire transport (FreeBSD-only) --- struct Response { diff --git a/lib/privops_client_pure.cpp b/lib/privops_client_pure.cpp index 2c6a51e..d5575d8 100644 --- a/lib/privops_client_pure.cpp +++ b/lib/privops_client_pure.cpp @@ -268,4 +268,24 @@ PrivOpsNvPure::FieldMap buildCreateEpair() { }; } +PrivOpsNvPure::FieldMap buildSetLoginclassRctl(const std::string &loginclass, + const std::string &key, + const std::string &rawValue) { + return { + {"verb", "set_loginclass_rctl"}, + {"loginclass", loginclass}, + {"key", key}, + {"value", rawValue}, + }; +} + +PrivOpsNvPure::FieldMap buildClearLoginclassRctl(const std::string &loginclass, + const std::string &key) { + return { + {"verb", "clear_loginclass_rctl"}, + {"loginclass", loginclass}, + {"key", key}, + }; +} + } // namespace PrivOpsClient diff --git a/lib/privops_nv_pure.cpp b/lib/privops_nv_pure.cpp index b16d8bf..80e3287 100644 --- a/lib/privops_nv_pure.cpp +++ b/lib/privops_nv_pure.cpp @@ -239,6 +239,21 @@ std::string parseCreateEpair(const FieldMap &/*m*/, return ""; } +std::string parseSetLoginclassRctl(const FieldMap &m, + PrivOpsPure::SetLoginclassRctlReq &out) { + if (auto e = requireString(m, "loginclass", out.loginclass); !e.empty()) return e; + if (auto e = requireString(m, "key", out.key); !e.empty()) return e; + if (auto e = requireString(m, "value", out.rawValue); !e.empty()) return e; + return ""; +} + +std::string parseClearLoginclassRctl(const FieldMap &m, + PrivOpsPure::ClearLoginclassRctlReq &out) { + if (auto e = requireString(m, "loginclass", out.loginclass); !e.empty()) return e; + if (auto e = requireString(m, "key", out.key); !e.empty()) return e; + return ""; +} + // --- Verb routing --- PrivOpsPure::Verb extractVerb(const FieldMap &m) { diff --git a/lib/privops_nv_pure.h b/lib/privops_nv_pure.h index 774d59a..7601963 100644 --- a/lib/privops_nv_pure.h +++ b/lib/privops_nv_pure.h @@ -131,6 +131,10 @@ std::string parseSetIfaceInetAddr(const FieldMap &m, PrivOpsPure::SetIfaceInetAddrReq &out); std::string parseCreateEpair(const FieldMap &m, PrivOpsPure::CreateEpairReq &out); +std::string parseSetLoginclassRctl(const FieldMap &m, + PrivOpsPure::SetLoginclassRctlReq &out); +std::string parseClearLoginclassRctl(const FieldMap &m, + PrivOpsPure::ClearLoginclassRctlReq &out); // --- Verb routing --- diff --git a/lib/privops_pure.cpp b/lib/privops_pure.cpp index 4b67874..4baec56 100644 --- a/lib/privops_pure.cpp +++ b/lib/privops_pure.cpp @@ -2,6 +2,7 @@ #include "privops_pure.h" +#include "per_user_rctl_pure.h" #include "retune_pure.h" #include @@ -91,6 +92,8 @@ const char *verbName(Verb v) { case Verb::BridgeDelMember: return "bridge_del_member"; case Verb::SetIfaceInetAddr: return "set_iface_inet_addr"; case Verb::CreateEpair: return "create_epair"; + case Verb::SetLoginclassRctl: return "set_loginclass_rctl"; + case Verb::ClearLoginclassRctl: return "clear_loginclass_rctl"; case Verb::Unknown: return "unknown"; } return "unknown"; @@ -117,6 +120,8 @@ Verb parseVerb(const std::string &name) { if (name == "bridge_del_member") return Verb::BridgeDelMember; if (name == "set_iface_inet_addr") return Verb::SetIfaceInetAddr; if (name == "create_epair") return Verb::CreateEpair; + if (name == "set_loginclass_rctl") return Verb::SetLoginclassRctl; + if (name == "clear_loginclass_rctl") return Verb::ClearLoginclassRctl; return Verb::Unknown; } @@ -513,4 +518,19 @@ std::string validateCreateEpair(const CreateEpairReq &) { return ""; } +std::string validateSetLoginclassRctl(const SetLoginclassRctlReq &r) { + if (auto e = PerUserRctlPure::validateLoginclassName(r.loginclass); !e.empty()) + return "loginclass: " + e; + if (auto e = RetunePure::validateRctlKey(r.key); !e.empty()) return e; + if (auto e = RetunePure::validateRctlValue(r.key, r.rawValue); !e.empty()) return e; + return ""; +} + +std::string validateClearLoginclassRctl(const ClearLoginclassRctlReq &r) { + if (auto e = PerUserRctlPure::validateLoginclassName(r.loginclass); !e.empty()) + return "loginclass: " + e; + if (auto e = RetunePure::validateRctlKey(r.key); !e.empty()) return e; + return ""; +} + } // namespace PrivOpsPure diff --git a/lib/privops_pure.h b/lib/privops_pure.h index 34f2895..95efd25 100644 --- a/lib/privops_pure.h +++ b/lib/privops_pure.h @@ -133,6 +133,17 @@ enum class Verb { // and setupBridgeEpair (line 396) where the existing code // unpacks `auto epairPair = IfconfigOps::createEpair();`. CreateEpair, + + // 0.9.28: per-loginclass RCTL rules. Wraps + // `rctl -a loginclass:crate-:KEY:deny=VAL` semantics + // (PerUserRctlPure::loginclassName generates the loginclass). + // The set_rctl verb (0.9.0) is jail-scoped; this is the + // umbrella variant that aggregates resource use across all + // of a single operator's jails — alice can't exceed her + // total memoryuse cap regardless of how many jails she + // spawns. + SetLoginclassRctl, + ClearLoginclassRctl, }; // Returns the verb's canonical wire-format token (lowercase, no @@ -261,6 +272,20 @@ struct SetIfaceInetAddrReq { struct CreateEpairReq { }; +// 0.9.28: per-loginclass RCTL rules. Loginclass is the umbrella +// "crate-" generated by PerUserRctlPure; key + value +// validate via the same RetunePure whitelist as SetRctl. +struct SetLoginclassRctlReq { + std::string loginclass; // e.g. "crate-1000" + std::string key; // RCTL key (validated via RetunePure) + std::string rawValue; // value (validated via RetunePure) +}; + +struct ClearLoginclassRctlReq { + std::string loginclass; + std::string key; +}; + // --- Per-verb validators --- // // Each `validate*(req)` returns "" on success, otherwise a one-line @@ -289,6 +314,8 @@ std::string validateBridgeAddMember(const BridgeAddMemberReq &r); std::string validateBridgeDelMember(const BridgeDelMemberReq &r); std::string validateSetIfaceInetAddr(const SetIfaceInetAddrReq &r); std::string validateCreateEpair(const CreateEpairReq &r); +std::string validateSetLoginclassRctl(const SetLoginclassRctlReq &r); +std::string validateClearLoginclassRctl(const ClearLoginclassRctlReq &r); // --- Field-level validators (exposed for tests + reuse) --- // diff --git a/lib/privops_wire_pure.cpp b/lib/privops_wire_pure.cpp index 4927393..bfac9a5 100644 --- a/lib/privops_wire_pure.cpp +++ b/lib/privops_wire_pure.cpp @@ -376,6 +376,21 @@ std::string parseCreateEpair(const std::string &/*body*/, return ""; } +std::string parseSetLoginclassRctl(const std::string &body, + PrivOpsPure::SetLoginclassRctlReq &out) { + if (auto e = requireStringField(body, "loginclass", out.loginclass); !e.empty()) return e; + if (auto e = requireStringField(body, "key", out.key); !e.empty()) return e; + if (auto e = requireStringField(body, "value", out.rawValue); !e.empty()) return e; + return ""; +} + +std::string parseClearLoginclassRctl(const std::string &body, + PrivOpsPure::ClearLoginclassRctlReq &out) { + if (auto e = requireStringField(body, "loginclass", out.loginclass); !e.empty()) return e; + if (auto e = requireStringField(body, "key", out.key); !e.empty()) return e; + return ""; +} + // --- Verb routing helper --- PrivOpsPure::Verb parseVerbFromPath(const std::string &path) { @@ -672,6 +687,28 @@ std::string formatCreateEpairSuccess(const std::string &ifaceA, return o.str(); } +std::string formatSetLoginclassRctlSuccess(const std::string &loginclass, + const std::string &key, + const std::string &rawValue) { + std::ostringstream o; + o << "{\"set\":true" + << ",\"loginclass\":\"" << escape(loginclass) << "\"" + << ",\"key\":\"" << escape(key) << "\"" + << ",\"value\":\"" << escape(rawValue) << "\"" + << "}"; + return o.str(); +} + +std::string formatClearLoginclassRctlSuccess(const std::string &loginclass, + const std::string &key) { + std::ostringstream o; + o << "{\"cleared\":true" + << ",\"loginclass\":\"" << escape(loginclass) << "\"" + << ",\"key\":\"" << escape(key) << "\"" + << "}"; + return o.str(); +} + DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, const std::string &body) { using namespace PrivOpsPure; @@ -716,6 +753,10 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, return runVerb(body, v, parseSetIfaceInetAddr, validateSetIfaceInetAddr); case Verb::CreateEpair: return runVerb(body, v, parseCreateEpair, validateCreateEpair); + case Verb::SetLoginclassRctl: + return runVerb(body, v, parseSetLoginclassRctl, validateSetLoginclassRctl); + case Verb::ClearLoginclassRctl: + return runVerb(body, v, parseClearLoginclassRctl, validateClearLoginclassRctl); case Verb::Unknown: break; } diff --git a/lib/privops_wire_pure.h b/lib/privops_wire_pure.h index 23ae800..799f4b2 100644 --- a/lib/privops_wire_pure.h +++ b/lib/privops_wire_pure.h @@ -174,6 +174,12 @@ std::string parseSetIfaceInetAddr(const std::string &body, std::string parseCreateEpair(const std::string &body, PrivOpsPure::CreateEpairReq &out); +std::string parseSetLoginclassRctl(const std::string &body, + PrivOpsPure::SetLoginclassRctlReq &out); + +std::string parseClearLoginclassRctl(const std::string &body, + PrivOpsPure::ClearLoginclassRctlReq &out); + // --- Verb routing helper --- // // Parse the URL path's verb segment. The route pattern is @@ -303,4 +309,11 @@ std::string formatSetIfaceInetAddrSuccess(const std::string &ifname, std::string formatCreateEpairSuccess(const std::string &ifaceA, const std::string &ifaceB); +// 0.9.28: 200 OK bodies for loginclass-scoped RCTL ops. +std::string formatSetLoginclassRctlSuccess(const std::string &loginclass, + const std::string &key, + const std::string &rawValue); +std::string formatClearLoginclassRctlSuccess(const std::string &loginclass, + const std::string &key); + } // namespace PrivOpsWirePure diff --git a/tests/unit/privops_pure_test.cpp b/tests/unit/privops_pure_test.cpp index 8173c59..de57d52 100644 --- a/tests/unit/privops_pure_test.cpp +++ b/tests/unit/privops_pure_test.cpp @@ -26,6 +26,7 @@ ATF_TEST_CASE_BODY(verb_token_roundtrips_for_every_verb) { Verb::SetIfaceUp, Verb::DisableIfaceOffload, Verb::BridgeAddMember, Verb::BridgeDelMember, Verb::SetIfaceInetAddr, Verb::CreateEpair, + Verb::SetLoginclassRctl, Verb::ClearLoginclassRctl, }; for (Verb v : verbs) { std::string token = verbName(v); @@ -547,6 +548,41 @@ ATF_TEST_CASE_BODY(create_epair_no_fields_required) { ATF_REQUIRE_EQ(validateCreateEpair(r), std::string()); } +ATF_TEST_CASE_WITHOUT_HEAD(set_loginclass_rctl_validates); +ATF_TEST_CASE_BODY(set_loginclass_rctl_validates) { + SetLoginclassRctlReq r; + r.loginclass = "crate-1000"; + r.key = "memoryuse"; + r.rawValue = "4G"; + ATF_REQUIRE_EQ(validateSetLoginclassRctl(r), std::string()); + + // Bad loginclass (no crate- prefix) + r.loginclass = "ops"; + ATF_REQUIRE(!validateSetLoginclassRctl(r).empty()); + + // Bad key (not in RCTL whitelist) + r.loginclass = "crate-1000"; + r.key = "totally-fake-key"; + ATF_REQUIRE(!validateSetLoginclassRctl(r).empty()); + + // pcpu out of range + r.key = "pcpu"; + r.rawValue = "200"; + ATF_REQUIRE(!validateSetLoginclassRctl(r).empty()); +} + +ATF_TEST_CASE_WITHOUT_HEAD(clear_loginclass_rctl_validates); +ATF_TEST_CASE_BODY(clear_loginclass_rctl_validates) { + ClearLoginclassRctlReq r; + r.loginclass = "crate-1000"; + r.key = "memoryuse"; + ATF_REQUIRE_EQ(validateClearLoginclassRctl(r), std::string()); + + // No value field on clear — but still validates loginclass + key + r.loginclass = "Bad-LoginClass"; + ATF_REQUIRE(!validateClearLoginclassRctl(r).empty()); +} + ATF_TEST_CASE_WITHOUT_HEAD(set_iface_inet_addr_minimal); ATF_TEST_CASE_BODY(set_iface_inet_addr_minimal) { SetIfaceInetAddrReq r; @@ -633,4 +669,6 @@ ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, bridge_del_member_minimal); ATF_ADD_TEST_CASE(tcs, set_iface_inet_addr_minimal); ATF_ADD_TEST_CASE(tcs, create_epair_no_fields_required); + ATF_ADD_TEST_CASE(tcs, set_loginclass_rctl_validates); + ATF_ADD_TEST_CASE(tcs, clear_loginclass_rctl_validates); }