217 lines
8.9 KiB
C++
217 lines
8.9 KiB
C++
/*
|
|
* capabilities.h
|
|
* Shared reader for the capability grant table (0:/config/capabilities.toml)
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#pragma once
|
|
#include <montauk/syscall.h>
|
|
#include <montauk/toml.h>
|
|
#include <montauk/config.h>
|
|
#include <montauk/heap.h>
|
|
|
|
/*
|
|
* 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.<id>." 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
|