/* * capabilities.h * Shared reader for the capability grant table (0:/config/capabilities.toml) * Copyright (c) 2026 Daniel Hammer */ #pragma once #include #include #include #include /* * Launchers (init, the desktop, the shell) look up the authority a program * should receive here instead of each carrying its own compiled-in table. * * This file is advisory, never authoritative. Every grant still goes * through SYS_SPAWN_CAPS and is validated in the kernel against the * caller's own delegable set, so nothing written here can produce authority * the kernel has not already delegated to the launcher. A missing, * truncated or hostile file can only ever result in a program receiving * less authority than intended. That is why the table can live in * userspace TOML: the kernel enumerates protected paths, userspace * interprets policy. * * Grants are keyed on the resolved binary path, which is what makes the * table safe to hand to init: pointing a privileged service entry at a * different executable looks up the new path, finds no entry, and grants * nothing. The kernel write-protects 0:/apps and 0:/os so the path cannot * be made to refer to a substituted image. */ namespace montauk { namespace caps { inline constexpr const char* GRANT_CONFIG = "capabilities"; inline constexpr const char* GRANT_PREFIX = "grant."; inline constexpr int MAX_SCAN_PROCS = 256; struct CapName { const char* name; uint64_t bit; }; // Names as they appear in the config file. Kept in the same order as the // CAP_* bit definitions in Api/Syscall.hpp. inline constexpr CapName NAMES[] = { {"process_admin", montauk::abi::CAP_PROCESS_ADMIN}, {"power_request", montauk::abi::CAP_POWER_REQUEST}, {"power_control", montauk::abi::CAP_POWER_CONTROL}, {"suspend", montauk::abi::CAP_SUSPEND}, {"storage_admin", montauk::abi::CAP_STORAGE_ADMIN}, {"raw_storage", montauk::abi::CAP_RAW_STORAGE}, {"network_admin", montauk::abi::CAP_NETWORK_ADMIN}, {"set_time", montauk::abi::CAP_SET_TIME}, {"user_admin", montauk::abi::CAP_USER_ADMIN}, {"display_admin", montauk::abi::CAP_DISPLAY_ADMIN}, {"device_admin", montauk::abi::CAP_DEVICE_ADMIN}, {"log_read", montauk::abi::CAP_LOG_READ}, {"system_image", montauk::abi::CAP_SYSTEM_IMAGE}, }; inline uint64_t bit_for_name(const char* name) { if (name == nullptr || name[0] == '\0') return 0; // "all" means "everything this launcher may pass on", which the // caller-delegable clamp in for_binary() then narrows. It excludes // CAP_SYSTEM_IMAGE: authority to rewrite a program image is never // something a wildcard should hand out, only an explicit name. if (montauk::streq(name, "all")) return montauk::abi::CAP_ALL & ~montauk::abi::CAP_SYSTEM_IMAGE; for (const auto& entry : NAMES) { if (montauk::streq(entry.name, name)) return entry.bit; } // Unknown names are ignored rather than rejected. Failing closed // costs a program some authority; failing open would hand out // authority nobody asked for. return 0; } // Read an array-of-strings key into a capability mask. A missing key is // an empty mask, which is the correct default for an absent grant. inline uint64_t mask_from_key(const montauk::toml::Doc& doc, const char* key) { montauk::toml::Value* arr = doc.get_array(key); if (arr == nullptr) return 0; uint64_t mask = 0; for (int i = 0; i < arr->array.count; i++) { montauk::toml::Value* item = arr->array.items[i]; if (item == nullptr || item->type != montauk::toml::Type::String) continue; mask |= bit_for_name(item->str); } return mask; } // Append `suffix` to the "grant.." stem of `path_key`. // Returns false if the key is not of that shape or does not fit. inline bool build_sibling_key(const char* path_key, const char* suffix, char* out, int outSz) { int prefixLen = 0; for (; GRANT_PREFIX[prefixLen]; prefixLen++) { if (path_key[prefixLen] != GRANT_PREFIX[prefixLen]) return false; } // Copy through the final '.' so "grant.foo.path" yields "grant.foo.". int lastDot = -1; for (int i = 0; path_key[i]; i++) { if (path_key[i] == '.') lastDot = i; } if (lastDot < prefixLen) return false; int n = 0; for (; n <= lastDot && n < outSz - 1; n++) out[n] = path_key[n]; for (int i = 0; suffix[i] && n < outSz - 1; i++) out[n++] = suffix[i]; out[n] = '\0'; return true; } // Look up the grant declared for `binary_path`. Returns false when the // path has no entry, which is the common case and means "no authority". inline bool lookup(const char* binary_path, montauk::abi::SpawnCapabilities& out) { out = {0, 0, 0}; if (binary_path == nullptr || binary_path[0] == '\0') return false; montauk::toml::Doc doc = montauk::config::load(GRANT_CONFIG); bool found = false; for (int i = 0; i < doc.entries.count && !found; i++) { montauk::toml::Value* entry = doc.entries.items[i]; if (entry == nullptr || entry->key == nullptr) continue; if (entry->type != montauk::toml::Type::String) continue; char sibling[128]; if (!build_sibling_key(entry->key, "path", sibling, sizeof(sibling))) continue; if (!montauk::streq(sibling, entry->key)) continue; if (!montauk::streq(entry->str, binary_path)) continue; build_sibling_key(entry->key, "effective", sibling, sizeof(sibling)); uint64_t effective = mask_from_key(doc, sibling); build_sibling_key(entry->key, "delegable", sibling, sizeof(sibling)); uint64_t delegable = mask_from_key(doc, sibling); build_sibling_key(entry->key, "permitted", sibling, sizeof(sibling)); uint64_t permitted = mask_from_key(doc, sibling); // A grant that does not name `permitted` owns exactly what it can // use or pass on. Declaring it separately is only needed by a // supervisor that holds authority in reserve (login). if (permitted == 0) permitted = effective | delegable; out.permitted = permitted; out.effective = effective; out.delegable = delegable; found = true; } doc.destroy(); return found; } // The calling process's own capability masks. // // There is no syscall to ask "what am I?", so this scans the process table // for our own PID. The buffer is heap-allocated because ProcInfo is large // enough that MAX_SCAN_PROCS of them would be a ~29 KB stack frame. inline bool self(montauk::abi::SpawnCapabilities& out) { out = {0, 0, 0}; auto* table = (montauk::abi::ProcInfo*)montauk::malloc( sizeof(montauk::abi::ProcInfo) * MAX_SCAN_PROCS); if (table == nullptr) return false; int count = montauk::proclist(table, MAX_SCAN_PROCS); int self_pid = montauk::getpid(); bool found = false; for (int i = 0; i < count; i++) { if (table[i].pid != self_pid) continue; out.permitted = table[i].permittedCaps; out.effective = table[i].effectiveCaps; out.delegable = table[i].delegableCaps; found = true; break; } montauk::mfree(table); return found; } inline uint64_t self_delegable() { montauk::abi::SpawnCapabilities mine; return self(mine) ? mine.delegable : 0; } // Build a spawn request for `binary_path`, clamped to what the caller may // actually delegate. The kernel enforces the same bound; clamping here // means a launcher that holds less authority than the table declares // degrades to a reduced grant instead of failing the spawn outright. inline montauk::abi::SpawnCapabilities for_binary(const char* binary_path, uint64_t caller_delegable) { montauk::abi::SpawnCapabilities caps{0, 0, 0}; // A caller with nothing to delegate cannot produce a non-empty grant, // so skip the file read entirely. This is the common case: every // unprivileged session, on every launch. if (caller_delegable == 0) return caps; if (!lookup(binary_path, caps)) return caps; caps.permitted &= caller_delegable; caps.effective &= caps.permitted; caps.delegable &= caps.permitted; return caps; } } // namespace caps } // namespace montauk